Merge "Replace wfGetLB"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 4 May 2018 19:08:32 +0000 (19:08 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 4 May 2018 19:08:32 +0000 (19:08 +0000)
82 files changed:
Gruntfile.js
RELEASE-NOTES-1.31
RELEASE-NOTES-1.32
includes/DefaultSettings.php
includes/MediaWiki.php
includes/Revision.php
includes/Setup.php
includes/api/i18n/es.json
includes/deferred/DeferredUpdates.php
includes/deferred/LinksUpdate.php
includes/installer/i18n/eu.json
includes/installer/i18n/ml.json
includes/libs/rdbms/lbfactory/ILBFactory.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/logging/LogPager.php
includes/parser/Sanitizer.php
includes/resourceloader/ResourceLoaderSkinModule.php
includes/skins/Skin.php
includes/skins/SkinTemplate.php
includes/specials/SpecialLog.php
includes/specials/SpecialRedirect.php
includes/specials/forms/EditWatchlistNormalHTMLForm.php
languages/classes/LanguageCrh.php
languages/data/CrhExceptions.php
languages/data/Names.php
languages/i18n/ace.json
languages/i18n/az.json
languages/i18n/bho.json
languages/i18n/ce.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/eu.json
languages/i18n/fi.json
languages/i18n/gl.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/io.json
languages/i18n/li.json
languages/i18n/lv.json
languages/i18n/min.json
languages/i18n/mk.json
languages/i18n/nah.json
languages/i18n/ps.json
languages/i18n/sd.json
languages/i18n/zgh.json
languages/messages/MessagesAbs.php [new file with mode: 0644]
package.json
resources/Resources.php
resources/lib/CLDRPluralRuleParser/CLDRPluralRuleParser.js [new file with mode: 0644]
resources/src/jquery/jquery.makeCollapsible.css
resources/src/jquery/jquery.makeCollapsible.js
resources/src/jquery/jquery.makeCollapsible.styles.less [new file with mode: 0644]
resources/src/jquery/jquery.tablesorter.less
resources/src/jquery/jquery.tablesorter.styles.less [new file with mode: 0644]
resources/src/mediawiki.libs.jpegmeta/export.js [new file with mode: 0644]
resources/src/mediawiki.libs.jpegmeta/jpegmeta.js [new file with mode: 0644]
resources/src/mediawiki.libs.pluralruleparser/export.js [new file with mode: 0644]
resources/src/mediawiki.libs/CLDRPluralRuleParser.js [deleted file]
resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js [deleted file]
resources/src/mediawiki/mediawiki.jqueryMsg.js
resources/src/mediawiki/mediawiki.js
resources/src/mediawiki/mediawiki.sectionAnchor.css [deleted file]
resources/src/mediawiki/mediawiki.util.js
resources/src/moment-dmy.js [deleted file]
resources/src/moment-global.js [deleted file]
resources/src/moment-locale-overrides.js [deleted file]
resources/src/moment/moment-dmy.js [new file with mode: 0644]
resources/src/moment/moment-global.js [new file with mode: 0644]
resources/src/moment/moment-locale-overrides.js [new file with mode: 0644]
tests/parser/ParserTestRunner.php
tests/phpunit/includes/api/ApiParseTest.php
tests/phpunit/includes/deferred/DeferredUpdatesTest.php
tests/phpunit/includes/parser/SanitizerTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderSkinModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
tests/phpunit/includes/skins/SkinTemplateTest.php
tests/phpunit/languages/classes/LanguageCrhTest.php
tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js
tests/selenium/.eslintrc.json
tests/selenium/README.md
tests/selenium/selenium.sh

index 69a123c..2f55868 100644 (file)
@@ -13,7 +13,6 @@ module.exports = function ( grunt ) {
        grunt.loadNpmTasks( 'grunt-jsonlint' );
        grunt.loadNpmTasks( 'grunt-karma' );
        grunt.loadNpmTasks( 'grunt-stylelint' );
-       grunt.loadNpmTasks( 'grunt-webdriver' );
 
        karmaProxy[ wgScriptPath ] = {
                target: wgServer + wgScriptPath,
@@ -29,7 +28,7 @@ module.exports = function ( grunt ) {
                                '!resources/lib/**',
                                '!resources/src/jquery.tipsy/**',
                                '!resources/src/jquery/jquery.farbtastic.js',
-                               '!resources/src/mediawiki.libs/**',
+                               '!resources/src/mediawiki.libs.jpegmeta/**',
                                // Third-party code of PHPUnit coverage report
                                '!tests/coverage/**',
                                '!vendor/**',
@@ -104,15 +103,7 @@ module.exports = function ( grunt ) {
                                        return require( 'path' ).join( dest, src.replace( 'resources/', '' ) );
                                }
                        }
-               },
-
-               // Configure WebdriverIO task
-               webdriver: {
-                       test: {
-                               configFile: './tests/selenium/wdio.conf.js'
-                       }
                }
-
        } );
 
        grunt.registerTask( 'assert-mw-env', function () {
index ae59234..a702451 100644 (file)
@@ -5,61 +5,54 @@ THIS IS NOT A RELEASE YET
 MediaWiki 1.31 is an alpha-quality branch and is not recommended for use in
 production.
 
-=== Important pre-upgrade notes for 1.31 ===
-* If you're using MySQL, SQLite, or MSSQL, are not using update.php to apply
-  schema changes, and cannot have downtime to run migrateArchiveText.php and
-  apply patch-drop-ar_text.sql manually, you'll have to apply a default value
-  to the ar_text and ar_flags columns of the archive table or make those
-  columns nullable before upgrading to MediaWiki 1.31.
-  maintenance/archives/patch-nullable-ar_text.sql shows how to do this for MySQL.
-
 === Configuration changes in 1.31 ===
 * $wgEnableAPI and $wgEnableWriteAPI are now deprecated and will be removed in
   a future version. The API is now considered to be stable, secure and
   essential.
-* $wgUsejQueryThree was removed, as it is now the default. This was documented as a
-  temporary variable during the migration period, deprecated since 1.29.
+* $wgUsejQueryThree was removed, as it is now the default. This was documented
+  as a temporary variable during the migration period, deprecated since 1.29.
 * $wgLogoHD has been updated to support svg images and uses $wgLogo where
   possible for fallback images such as png.
-* (T44246) $wgFilterLogTypes will no longer ignore 'patrol' when user does
-  not have the right to mark things patrolled.
+* (T44246) $wgFilterLogTypes will no longer ignore 'patrol' when user does not
+  have the right to mark things patrolled.
 * Wikis that contain imported revisions or CentralAuth global blocks should run
   maintenance/cleanupUsersWithNoId.php.
-* $wgResourceLoaderMinifierStatementsOnOwnLine and $wgResourceLoaderMinifierMaxLineLength
-  were removed (deprecated since 1.27).
-* (T180921) $wgReferrerPolicy now supports having fallbacks for browsers that are not
-  using the latest version of the Referrer Policy specification.
-* $wgFragmentMode is now set to [ 'legacy', 'html5' ] by default. This is a first step of
-  migration to human-readable section IDs that will later result in 'html5' being the
-  default mode.
+* The configuration settings $wgResourceLoaderMinifierStatementsOnOwnLine and
+  $wgResourceLoaderMinifierMaxLineLength, deprecated since 1.27, were removed.
+* (T180921) $wgReferrerPolicy now supports having fallbacks for browsers that
+  are not using the latest version of the Referrer Policy specification.
+* $wgFragmentMode is now set to [ 'legacy', 'html5' ] by default. This is a
+  first step of migration to human-readable section IDs that will later result
+  in 'html5' being the default mode.
 * CACHE_ACCEL now only supports APC(u) or WinCache. XCache support was removed
   as upstream is inactive and has no plans to move to PHP 7.
 * The old CategorizedRecentChanges feature, including its related configuration
   option $wgAllowCategorizedRecentChanges, has been removed.
-* (T188472) The 'comma' value for $wgArticleCountMethod is no longer supported for
-  performance reasons, and installations with this setting will now work as if it
-  was configured with 'any'.
-* (T185753) MediaWiki now defaults to using RemexHtml to tidy up user input, rather than
-  being off by default. If you wish to disable HTML tidying entirely, set $wgTidyConfig
-  to null; if you wish to use the old, deprecated Tidy external binary, both
-  set $wgTidyConfig to null and also set $wgUseTidy to true.
+* (T188472) The 'comma' value for $wgArticleCountMethod is no longer supported
+  for performance reasons, and installations with this setting will now work as
+  if it was configured with 'any'.
+* (T185753) MediaWiki now defaults to using RemexHtml to tidy up user input,
+  rather than being off by default. If you wish to disable HTML tidying
+  entirely, set $wgTidyConfig to null; if you wish to use the old, deprecated
+  Tidy external binary, both set $wgTidyConfig to null and $wgUseTidy to true.
 * $wgLogAutopatrol now defaults to false instead of true.
 * $wgValidateAllHtml was removed and will be ignored.
-* $wgScriptExtension was removed (deprecated and ignored since 1.25).
-  See 1.25 release notes for more information.
+* $wgScriptExtension, deprecated and ignored since 1.25, was removed. See the
+  1.25 release notes for more information.
 * $wgUseAjax is now marked as deprecated, just like the deprecated AJAX
   framework that it enables. Some extensions mistakenly used this to check
   whether any AJAX functionality at all should be enabled, further making this
   problematic to retain.
 
 === New features in 1.31 ===
-* (T76554) User sub-pages named ….json are now protected in the same way that ….js
-  and ….css pages are, so that configuration options can safely be placed there.
-* Wikimedia\Rdbms\IDatabase->select() and similar methods now support
-  joins with parentheses for grouping.
+* (T76554) User sub-pages named ….json are now protected in the same way that
+  ….js and ….css pages are, so that configuration options can safely be placed
+  there.
+* Wikimedia\Rdbms\IDatabase->select() and similar methods now support joins
+  with parentheses for grouping.
 * As a first pass in standardizing dialog boxes across the MediaWiki product,
-  Html class now provides helper methods for messageBox, successBox, errorBox and
-  warningBox generation.
+  Html class now provides helper methods for messageBox, successBox, errorBox
+  and warningBox generation.
 * (T9240) Imports will now record unknown (and, optionally, known) usernames in
   a format like "iw>Example".
 * (T20209) Linker (used on history pages, log pages, and so on) will display
@@ -85,9 +78,9 @@ production.
     soon as any necessary extensions are updated.
   * Most code accessing rows for logged actions from the database should use
     the relevant getQueryInfo() methods to get the information needed to build
-    the SQL query. The ActorMigration class may also be used to get feature-flagged
-    information needed to access actor-related fields during the migration
-    period.
+    the SQL query. The ActorMigration class may also be used to get feature
+    -flagged information needed to access actor-related fields during the
+    migration period.
 * Added Wikimedia\Rdbms\IDatabase::cancelAtomic(), to roll back an atomic
   section without having to roll back the whole transaction.
 * Wikimedia\Rdbms\IDatabase::doAtomicSection(), non-native ::insertSelect(),
@@ -98,21 +91,21 @@ production.
   extensions. Pass --with-extensions to enable that feature.
 * (T184791) rc_patrolled now has three states: "0" for unpatrolled,
   "1" for manually patrolled and "2" for autopatrolled actions.
-* Extensions can now set their type to "editor" if they provide an editor
-  or enhance the editing experience.
-* Extensions can use a PSR-4 autoloader by setting an "AutoloadNamespaces" property
-  in extension.json. See
-  <https://www.mediawiki.org/wiki/Manual:Extension.json/Schema#AutoloadNamespaces>
+* Extensions can now set their type to "editor" if they provide an editor or
+  enhance the editing experience.
+* Extensions can use a PSR-4 autoloader by setting an "AutoloadNamespaces"
+  property in extension.json. See the documentation at
+  <https://mediawiki.org/wiki/Manual:Extension.json/Schema#AutoloadNamespaces>
   for more details and an example.
+* (T19099) Tabs which link to pages that don't exist (like those to uncreated
+  discussion pages) now have a tooltip to indicate state, not just colour.
 
 === External library changes in 1.31 ===
 
 ==== Upgraded external libraries ====
 * Updated jquery.chosen from v0.9.14 to v1.8.2.
-* Updated composer/spdx-licenses from 1.1.4 to
-  1.3.0 (development dependency).
-* Updated nikic/php-parser from 2.1.0 to 3.1.3
-  (development dependency).
+* Updated composer/spdx-licenses from 1.1.4 to 1.3.0 (development dependency).
+* Updated nikic/php-parser from 2.1.0 to 3.1.3 (development dependency).
 * Updated wikimedia/ip-set from 1.1.0 to 1.2.0.
 * Updated wikimedia/relpath from 2.0.0 to 2.1.1.
 * Updated wikimedia/running-stat from 1.1.0 to 1.2.0.
@@ -121,11 +114,9 @@ production.
 * Updated wikimedia/php-session-serializer from 1.0.4 to 1.0.6.
 * Updated wikimedia/remex-html from 1.0.2 to 1.0.3.
 * Updated wikimedia/html-formatter from 1.0.1 to 1.0.2.
-* …
 
 ==== New external libraries ====
 * Added wikimedia/object-factory 1.0.0
-* …
 
 ==== Removed and replaced external libraries ====
 * (T17845) The deprecated 'jquery.badge' module was removed.
@@ -134,33 +125,35 @@ production.
 * The deprecated 'jquery.placeholder' module was removed.
 * The deprecated 'jquery.appear' module was removed. Use the
   'mediawiki.viewport' module instead.
-* The deprecated 'mediawiki.widgets.CategorySelector' module alias was removed.
-  Use the 'mediawiki.widgets.CategoryMultiselectWidget' module directly instead.
 * mediawiki/at-ease was replaced with wikimedia/at-ease.
 
 === Bug fixes in 1.31 ===
 * (T90902) Non-breaking space in header ID breaks anchor.
-* (T189375) CSSMin now allows quoted urls in `url()` syntax to start with a space.
+* (T189375) CSSMin now allows quoted urls in `url()` syntax to start with a
+  space.
+* (T2087, T10897, T87753, T174639) Whitespace created by category and language
+  links is now stripped rather than leaving blank lines in odd places.
+* (T3780) Uploads with UTF-8 names now work on PHP7.1+ on Windows servers.
 
 === Action API changes in 1.31 ===
 * (T185058) The 'name' value to tgprop for action=query&list=tags has been
   removed. It has never made a difference in the output, the name was always
   returned regardless.
-* The 'watch' and 'unwatch' parameters for action=move have been removed.  They
-  were deprecated and also accidentally nonfunctional since 1.17 in 2010.  Use
+* The 'watch' and 'unwatch' parameters for action=move have been removed. They
+  were deprecated and also accidentally nonfunctional since 1.17 in 2010. Use
   'watchlist' instead.
 
 === Action API internal changes in 1.31 ===
-* ApiBase::getProfileDBTime was removed (deprecated since 1.25)
-* ApiBase::getModuleProfileName was removed (deprecated since 1.25)
-* ApiBase::getProfileTime was removed (deprecated since 1.25)
+* ApiBase::getProfileDBTime, deprecated since 1.25, was removed.
+* ApiBase::getModuleProfileName, deprecated since 1.25, was removed.
+* ApiBase::getProfileTime, deprecated since 1.25, was removed.
 
 === Languages updated in 1.31 ===
 MediaWiki supports over 350 languages. Many localisations are updated
 regularly. Below only new and removed languages are listed, as well as
 changes to languages because of Phabricator reports.
 
-* (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK namespaces.
+* (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK.
 * (T182305) New language support: Nyungar (nys).
 * (T186359) New language support: Siberian Tatar [cебертатар] (sty).
 * (T186635) New language support: Guianan Creole (gcr).
@@ -170,17 +163,16 @@ changes to languages because of Phabricator reports.
 * (T189127) New language support: Gorontalo (gor).
 
 === Breaking changes in 1.31 ===
-* MessageBlobStore::insertMessageBlob() (deprecated in 1.27) was removed.
-* The OutputPage class constructor now requires a context parameter,
-  (instantiating without context was deprecated in 1.18)
-* The mw.page JavaScript singleton (deprecated in 1.30) was removed.
+* MessageBlobStore::insertMessageBlob(), deprecated in 1.27, was removed.
+* The OutputPage class constructor now requires a context parameter.
+  Instantiating without context was deprecated in 1.18.
+* The mw.page JavaScript singleton, deprecated in 1.30, was removed.
 * Article::getLastPurgeTimestamp(), WikiPage::getLastPurgeTimestamp(), and the
   related WikiPage::PURGE_* constants, deprecated in 1.29, were removed.
-* The Article::selectFields(), Article::onArticleCreate(),
-  Article::onArticleDelete(), and Article::onArticleEdit() methods, deprecated
-  in 1.24, were removed.
-* Installer::locateExecutable() and Installer::locateExecutableInDefaultPaths()
-  were removed, use ExecutableFinder::findInDefaultPaths() instead.
+* The Article::selectFields(), ::onArticleCreate(), ::onArticleDelete(), and
+  ::onArticleEdit() methods, deprecated in 1.24, were removed.
+* Installer::locateExecutable() and ::locateExecutableInDefaultPaths() were
+  removed. Use ExecutableFinder::findInDefaultPaths() instead.
 * The deprecated MW_DIFF_VERSION constant was removed.
   DifferenceEngine::MW_DIFF_VERSION should be used instead.
 * Due to significant refactoring, method ContribsPager::getUserCond() that had
@@ -188,8 +180,8 @@ changes to languages because of Phabricator reports.
 * The Block class will no longer accept usable-but-missing usernames for
   'byText' or ->setBlocker(). Callers should either ensure the blocker exists
   locally or use a new interwiki-format username like "iw>Example".
-* The following methods and constants from the WatchedItem class, which were deprecated in
-  1.27, have been removed.
+* The following methods and constants from the WatchedItem class, which were
+  deprecated in 1.27, have been removed:
   * WatchedItem::getTitle()
   * WatchedItem::fromUserTitle()
   * WatchedItem::addWatch()
@@ -200,22 +192,24 @@ changes to languages because of Phabricator reports.
   * WatchedItem::CHECK_USER_RIGHTS
   * WatchedItem::DEPRECATED_USAGE_TIMESTAMP
 * The $statementsOnOwnLine parameter of JavaScriptMinifier::minify was removed.
-  The corresponding configuration variable ($wgResourceLoaderMinifierStatementsOnOwnLine)
-  has been deprecated since 1.27 and was removed as well.
+  $wgResourceLoaderMinifierStatementsOnOwnLine, the corresponding configuration
+  variable, has been deprecated since 1.27 and was removed as well.
 * The $maxLineLength parameter of JavaScriptMinifier::minify was removed.
-  The corresponding configuration variable ($wgResourceLoaderMinifierMaxLineLength)
-  has been deprecated since 1.27 and was removed as well.
-* The HtmlFormatter class was removed (deprecated in 1.27). The namespaced
+  $wgResourceLoaderMinifierMaxLineLength, the corresponding configuration
+  variable, has been deprecated since 1.27 and was removed as well.
+* The HtmlFormatter class, deprecated in 1.27, was removed. The namespaced
   HtmlFormatter\HtmlFormatter class should be used instead.
 * The driver 'mysql' for MySQL, deprecated in MediaWiki 1.30, has been removed.
   The driver has been deprecated since PHP 5.5 and was removed in PHP 7.0. The
   default driver for MySQL has been 'mysqli' since MediaWiki 1.22.
-* The following properties of PreparedEdit were deprecated in 1.21 and have been removed:
+* The following properties of PreparedEdit were deprecated in 1.21 and have
+  been removed:
   * PreparedEdit->newText
   * PreparedEdit->oldText
   * PreparedEdit->pst
-* ParserOutput objects generated using a non-default value for
-  ParserOptions::setWrapOutputClass() can no longer be added to the parser cache.
+* ParserOutput objects which are generated using a non-default value for
+  ParserOptions::setWrapOutputClass() can no longer be added to the parser
+  cache.
 * The following deprecated methods from the OutputPage class have been removed:
   * OutputPage::addExtensionStyle(); deprecated in 1.27
   * OutputPage::getExtStyle(); deprecated in 1.27
@@ -223,69 +217,78 @@ changes to languages because of Phabricator reports.
   * OutputPage::setSquidMaxage(); deprecated in 1.27
   * OutputPage::readOnlyPage(); deprecated in 1.25
   * OutputPage::rateLimited(); deprecated in 1.25
-  * Additionally, the protected OutputPage::$mExtStyles array, only accessed through
-    the above and with no known uses, was removed.
+  * Additionally, the protected OutputPage::$mExtStyles array, only accessed
+    through the above and with no known uses, was removed.
 * The no-op method Skin::showIPinHeader(), deprecated in 1.27, was removed.
-* The following variables and methods in EditPage, deprecated in MediaWiki 1.30, were removed:
+* The following variables and methods in EditPage, deprecated in MediaWiki 1.30,
+  were removed:
   * $isCssJsSubpage — use ::isUserConfigPage()
   * $isCssSubpage — use ::isUserCssConfigPage()
   * $isJsSubpage — use ::isUserJsConfigPage()
-  * $isWrongCaseCssJsPage – use ::isWrongCaseUserConfigPage()
-  * ::getSummaryInput() – use ::getSummaryInputWidget()
-  * ::getSummaryInputOOUI() – use ::getSummaryInputWidget()
-  * ::getCheckboxes() – use ::getCheckboxesWidget() or ::getCheckboxesDefinition()
-  * ::getCheckboxesOOUI() – use ::getCheckboxesWidget() or ::getCheckboxesDefinition()
-* The method ResourceLoaderModule::getPosition(), deprecated in 1.29, has been removed.
-* In User, the cookie-related methods which were wrappers for the functions on the response
-  object, and were deprecated in 1.27, have been removed:
+  * $isWrongCaseCssJsPage – use ::isWrongCaseUserConfigPage()
+  * ::getSummaryInput() – use ::getSummaryInputWidget()
+  * ::getSummaryInputOOUI() – use ::getSummaryInputWidget()
+  * ::getCheckboxes() – use ::getCheckboxesWidget() or
+      ::getCheckboxesDefinition()
+  * ::getCheckboxesOOUI() – use ::getCheckboxesWidget() or
+      ::getCheckboxesDefinition()
+* ResourceLoaderModule::getPosition(), deprecated in 1.29, has been removed.
+* In User, the cookie-related methods which were wrappers for the functions on
+  the response object, and were deprecated in 1.27, have been removed:
   * ::setCookie()
   * ::clearCookie()
   * ::setExtendedLoginCookie()
   Note that User::setCookies() remains, and is not deprecated.
-* Also in User, some auth-related methods which were deprecated in 1.27, have been removed:
-  * ::getEditTokenTimestamp() – use MediaWiki\Session\Token::getTimestamp()
-  * ::getPasswordFactory() – create a PasswordFactory directly
+* Also in User, some auth-related methods which were deprecated in 1.27 have
+  been removed:
+  * ::getEditTokenTimestamp() – use MediaWiki\Session\Token::getTimestamp()
+  * ::getPasswordFactory() – create a PasswordFactory directly
   * ::passwordChangeInputAttribs()
-* The global functions wfProfileIn and wfProfileOut, deprecated in 1.25, have been removed.
+* The global functions wfProfileIn and wfProfileOut, deprecated in 1.25, have
+  been removed.
 * SpecialPageFactory::getList(), deprecated in 1.24, has been removed. You can
   use ::getNames() instead.
 * OpenSearch::getOpenSearchTemplate(), deprecated in 1.25, has been removed. You
   can use ApiOpenSearch::getOpenSearchTemplate() instead.
 * The global function wfBaseConvert, deprecated in 1.27, has been removed. Use
   Wikimedia\base_convert() directly.
-* Calling Database::begin() explicitly during an implicit transaction or when DBO_TRX
-  is set results in an exception. Calling Database::commit() explicitly for an implicit
-  transaction also results in an exception. Previously these were logged as errors.
-  The startAtomic() and endAtomic() methods, or AtomicSectionUpdate should be used
-  instead.
+* Calling Database::begin() explicitly during an implicit transaction or when
+  DBO_TRX is set results in an exception. Calling Database::commit() explicitly
+  for an implicit transaction also results in an exception. Previously these
+  were logged as errors. The startAtomic() and endAtomic() methods, or
+  AtomicSectionUpdate should be used instead.
 * The global function wfOutputHandler() was removed, use the its replacement
-  MediaWiki\OutputHandler::handle() instead. The global function was only sometimes defined.
-  Its replacement is always available via the autoloader.
-* ChangeTags::listExtensionActivatedTags and ::listExtensionDefinedTags, deprecated
-  in 1.28, have been removed.  Use ::listSoftwareActivatedTags() and
+  MediaWiki\OutputHandler::handle() instead. The global function was only
+  sometimes defined. Its replacement is always available via the autoloader.
+* ChangeTags::listExtensionActivatedTags and ::listExtensionDefinedTags,
+  deprecated in 1.28, have been removed. Use ::listSoftwareActivatedTags() and
   ::listSoftwareDefinedTags() instead.
-* Title::getTitleInvalidRegex(), deprecated in 1.25, has been removed. You
-  can use MediaWikiTitleCodec::getTitleInvalidRegex() instead.
+* Title::getTitleInvalidRegex(), deprecated in 1.25, has been removed. You can
+  use MediaWikiTitleCodec::getTitleInvalidRegex() instead.
 * HTMLForm & VFormHTMLForm::isVForm(), deprecated in 1.25, have been removed.
 * The ProfileSection class, deprecated in 1.25 and unused, has been removed.
-* The ResourceLoaderGetLessVars hook, deprecated in 1.30, has been removed.
-  Use ResourceLoaderModule::getLessVars() to expose local variables instead
-  of global ones.
-* As part of work to modernise user-generated content clean-up, a config option and some
-  methods related to HTML validity were removed without deprecation. The public methods
-  MWTidy::checkErrors() and its callee TidyDriverBase::validate() are removed, as are
-  MediaWikiTestCase::assertValidHtmlSnippet() and ::assertValidHtmlDocument(). The
-  $wgValidateAllHtml configuration option is removed and will be ignored.
-* Execution of external programs using MediaWiki\Shell\Command now applies RESTRICT_DEFAULT
-  Firejail restriction by default.
+* The ResourceLoaderGetLessVars hook, deprecated in 1.30, has been removed. Use
+  ResourceLoaderModule::getLessVars() to expose local variables instead of
+  global ones.
+* As part of work to modernise user-generated content clean-up, a config option
+  and some methods related to HTML validity were removed without deprecation.
+  The public methods MWTidy::checkErrors() and the path through which it was
+  called, TidyDriverBase::validate(), are removed, as are the testing methods
+  MediaWikiTestCase::assertValidHtmlSnippet() and ::assertValidHtmlDocument().
+  The $wgValidateAllHtml configuration option is removed and will be ignored.
+* Execution of external programs using MediaWiki\Shell\Command now applies
+  the RESTRICT_DEFAULT Firejail restriction by default.
 * The ResourceLoaderModule::getHashMtime() and ::getDefinitionMtime() methods,
   deprecated in 1.26, were removed.
+* The deprecated 'mediawiki.widgets.CategorySelector' module alias was removed.
+  Use the 'mediawiki.widgets.CategoryMultiselectWidget' module directly.
 
 === Deprecations in 1.31 ===
 * The Revision class was deprecated in favor of RevisionStore, BlobStore, and
   RevisionRecord and its subclasses.
 * The global function wfBCP47 is deprecated in favour of LanguageCode::bcp47.
-* The global function wfCountDown is now deprecated in favor of Maintenance::countDown.
+* The global function wfCountDown is now deprecated in favor of
+  Maintenance::countDown.
 * Several methods for returning lists of fields to select from the database
   have been deprecated in favor of similar methods that also return the tables
   to select from and the join conditions for those tables.
@@ -314,9 +317,9 @@ changes to languages because of Phabricator reports.
 * Use of Maintenance::error( $err, $die ) to exit script was deprecated. Use
   Maintenance::fatalError() instead.
 * Passing a ParserOptions object to OutputPage::parserOptions() is deprecated.
-* The RevisionInsertComplete hook is now deprecated, use RevisionRecordInserted instead.
-  RevisionInsertComplete is still called, but the second and third parameter will always be null.
-  Hard deprecation is scheduled for 1.32.
+* The RevisionInsertComplete hook is now deprecated; use instead the hook
+  RevisionRecordInserted. RevisionInsertComplete is still called, but the second
+  and third parameter will always be null. Hard deprecation is scheduled for 1.32.
 * The following methods that get and set ParserOutput state are deprecated.
   Callers should use the new stateless $options parameter to
   ParserOutput::getText() instead.
@@ -328,32 +331,39 @@ changes to languages because of Phabricator reports.
   * ParserOutput::setTOCEnabled()
   * OutputPage::enableSectionEditLinks()
   * OutputPage::sectionEditLinksEnabled()
-  * The public ParserOutput state fields $mTOCEnabled and $mEditSectionTokens are also deprecated.
+  * The public ParserOutput state fields $mTOCEnabled and $mEditSectionTokens
+    are also deprecated.
 * License::getLicenses has been deprecated; use License::getLines instead.
 * QuickTemplate::setRef() was deprecated in favour of QuickTemplate::set().
-  Setting template variables by reference allowed violating the principle of data being
-  immutable once added to the skin template. In practice, this method was not being
-  used for that. Rather, setRef() existed as memory optimisation for PHP 4.
-* QuickTemplate::setTranslator() was deprecated in favour of Skin::msg() parameters.
-* MediaWikiI18N::set() was deprecated in favour of Skin::msg() parameters.
-* MediaWikiI18N::translate() was deprecated in favour of Skin::msg() or wfMessage().
+  Setting template variables by reference allowed violating the principle of
+  data being immutable once added to the skin template. In practice, this method
+  was not being used for that. Rather, setRef() existed as memory optimisation
+  for PHP 4.
+* QuickTemplate::setTranslator() and MediaWikiI18N::set() were deprecated in
+  favour of Skin::msg() parameters.
+* MediaWikiI18N::translate() was deprecated in favour of Skin::msg() or
+  wfMessage().
 * Passing false to ParserOptions::setWrapOutputClass() is deprecated. Use the
   'unwrap' transform to ParserOutput::getText() instead.
-* \ObjectFactory (no namespace) is deprecated, the namespaced \Wikimedia\ObjectFactory
-  from the wikimedia/object-factory library should be used instead.
-* CommentStore::newKey is deprecated. Get an instance from MediaWikiServices instead.
-* The following CommentStore methods have had their signatures changed to introduce a $key parameter,
-  usage of the methods on instances retrieved from CommentStore::newKey will remain unchanged but deprecated:
+* \ObjectFactory (no namespace) is deprecated, the namespaced class
+  \Wikimedia\ObjectFactory from the wikimedia/object-factory library should be
+  used instead.
+* CommentStore::newKey is deprecated. Instead, get an instance from
+  MediaWikiServices.
+* The following CommentStore methods have had their signatures changed to
+  introduce a $key parameter, usage of the methods on instances retrieved from
+  CommentStore::newKey will remain unchanged but deprecated:
   * CommentStore::getFields
   * CommentStore::getJoin
   * CommentStore::getComment
   * CommentStore::getCommentLegacy
   * CommentStore::insert
   * CommentStore::insertWithTemplate
-* The following methods in Title have been renamed, and the old ones are deprecated:
-  * Title::getSkinFromCssJsSubpage – use ::getSkinFromConfigSubpage
-  * Title::isCssOrJsPage – use ::isSiteConfigPage
-  * Title::isCssJsSubpage – use ::isUserConfigPage
+* The following methods in Title have been renamed, and the old ones are
+  deprecated:
+  * Title::getSkinFromCssJsSubpage – use ::getSkinFromConfigSubpage
+  * Title::isCssOrJsPage – use ::isSiteConfigPage
+  * Title::isCssJsSubpage – use ::isUserConfigPage
   * Title::isCssSubpage – use ::isUserCssConfigPage
   * Title::isJsSubpage – use ::isUserJsConfigPage
 * The following methods related to caching of half-parsed HTML were deprecated:
@@ -370,22 +380,23 @@ changes to languages because of Phabricator reports.
   used instead.
 * The function wfShellWikiCmd() has been deprecated, use
   MediaWiki\Shell::makeScriptCommand().
-
 === Other changes in 1.31 ===
 * Browser support for Internet Explorer 10 was lowered from Grade A to Grade C.
-* Browser support for Opera 12 and older was removed. Opera 15+ continues at Grade A.
-* Introducing multi-content-revision capability into the storage layer. For details,
-  see <https://www.mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
-* The "free" CSS class is now only applied to unbracketed URLs in wikitext. Links
-  written using square brackets will get the class "text" not "free".
+* Browser support for Opera 12 and older was dropped entirely. Opera 15+
+  continues at Grade A.
+* Multi-content-revision capability was introduced into the storage layer. See
+  <https://mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
+* The "free" CSS class is now only applied to unbracketed URLs in wikitext.
+  Links written using square brackets will get the class "text" not "free".
 * RFC 157418: Whitespace is trimmed from wikitext headings, wikitext list items,
   wikitext table captions, wikitext table headings, wikitext table cells. HTML
-  headings, HTML list items, HTML table captions, HTML table headings, HTML table cells
-  will not have this trimming behavior.
+  headings, HTML list items, HTML table captions, HTML table headings, HTML
+  table cells will not have this trimming behavior.
 
 == Compatibility ==
-MediaWiki 1.31 requires PHP 5.5.9 or later. Although HHVM 3.18.5 or later is supported,
-it is generally advised to use PHP 5.5.9 or later for long term support.
+MediaWiki 1.31 requires PHP 7.0.0 or later. Although HHVM 3.18.5 or later is
+supported, it is generally advised to use PHP 7.0.0 or later for long term
+support.
 
 MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used,
 but support for them is somewhat less mature. There is experimental support for
index 92379c4..a437eb9 100644 (file)
@@ -11,6 +11,8 @@ production.
 * The $wgSiteSupportPage setting, unused since 1.5, was removed.
 * $wgJpegQuality was added to allow configuring the quality of JPEG thumbnails (default 80).
 * The default quality of JPEG thumbnails generated by GD was reduced from 95 to 80.
+* $wgExperimentalHtmlIds, deprecated since 1.30, has been removed. The 'html5-legacy' value for
+  $wgFragmentMode is no longer accepted.
 * …
 
 === New features in 1.32 ===
@@ -44,7 +46,7 @@ MediaWiki supports over 350 languages. Many localisations are updated
 regularly. Below only new and removed languages are listed, as well as
 changes to languages because of Phabricator reports.
 
-* 
+* (T193566) Added language support for Ambonese Malay (abs).
 
 === Breaking changes in 1.32 ===
 * $wgRequestTime was removed (deprecated in 1.25).
@@ -61,6 +63,13 @@ changes to languages because of Phabricator reports.
 * EditPage::isOouiEnabled() was removed (deprecated in 1.30).
 * mw.util.wikiGetlink() was removed (deprecated in 1.23).
   Use mw.util.getUrl() instead.
+* (T61113) The following methods and constants from the Revision class were deprecated in
+  1.25 and have now been removed.
+  * Revision::getRawUser()
+  * Revision::getRawUserText()
+  * Revision::getRawComment()
+* window.gM() from mediawiki.jqueryMsg was removed (deprecated in 1.23).
+  Use mw.msg() or mw.message() instead.
 
 === Deprecations in 1.32 ===
 * Use of a StartProfiler.php file is deprecated in favour of placing
index 6051511..2dc43fe 100644 (file)
@@ -3372,23 +3372,12 @@ $wgApiFrameOptions = 'DENY';
  */
 $wgDisableOutputCompression = false;
 
-/**
- * Abandoned experiment with HTML5-style ID escaping. Normalized IDs a bit
- * too aggressively, breaking preexisting content (particularly Cite).
- * See T29733, T29694, T29474.
- *
- * @deprecated since 1.30, use $wgFragmentMode
- */
-$wgExperimentalHtmlIds = false;
-
 /**
  * How should section IDs be encoded?
  * This array can contain 1 or 2 elements, each of them can be one of:
  * - 'html5'  is modern HTML5 style encoding with minimal escaping. Displays Unicode
  *            characters in most browsers' address bars.
  * - 'legacy' is old MediaWiki-style encoding, e.g. 啤酒 turns into .E5.95.A4.E9.85.92
- * - 'html5-legacy' corresponds to DEPRECATED $wgExperimentalHtmlIds mode. DO NOT use
- *            it for anything but migration off that mode (see below).
  *
  * The first element of this array specifies the primary mode of escaping IDs. This
  * is what users will see when they e.g. follow an [[#internal link]] to a section of
index 82ffcfb..e6dc0fe 100644 (file)
@@ -998,8 +998,14 @@ class MediaWiki {
         * @param LoggerInterface $runJobsLogger
         */
        private function triggerSyncJobs( $n, LoggerInterface $runJobsLogger ) {
-               $runner = new JobRunner( $runJobsLogger );
-               $runner->run( [ 'maxJobs' => $n ] );
+               $trxProfiler = Profiler::instance()->getTransactionProfiler();
+               $old = $trxProfiler->setSilenced( true );
+               try {
+                       $runner = new JobRunner( $runJobsLogger );
+                       $runner->run( [ 'maxJobs' => $n ] );
+               } finally {
+                       $trxProfiler->setSilenced( $old );
+               }
        }
 
        /**
index 652ce4d..548ef8d 100644 (file)
@@ -786,17 +786,6 @@ class Revision implements IDBAccessObject {
                return $user ? $user->getId() : 0;
        }
 
-       /**
-        * Fetch revision's user id without regard for the current user's permissions
-        *
-        * @return int
-        * @deprecated since 1.25, use getUser( Revision::RAW )
-        */
-       public function getRawUser() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return $this->getUser( self::RAW );
-       }
-
        /**
         * Fetch revision's username if it's available to the specified audience.
         * If the specified audience does not have access to the username, an
@@ -820,18 +809,6 @@ class Revision implements IDBAccessObject {
                $user = $this->mRecord->getUser( $audience, $user );
                return $user ? $user->getName() : '';
        }
-
-       /**
-        * Fetch revision's username without regard for view restrictions
-        *
-        * @return string
-        * @deprecated since 1.25, use getUserText( Revision::RAW )
-        */
-       public function getRawUserText() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return $this->getUserText( self::RAW );
-       }
-
        /**
         * Fetch revision comment if it's available to the specified audience.
         * If the specified audience does not have access to the comment, an
@@ -856,17 +833,6 @@ class Revision implements IDBAccessObject {
                return $comment === null ? null : $comment->text;
        }
 
-       /**
-        * Fetch revision comment without regard for the current user's permissions
-        *
-        * @return string
-        * @deprecated since 1.25, use getComment( Revision::RAW )
-        */
-       public function getRawComment() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return $this->getComment( self::RAW );
-       }
-
        /**
         * @return bool
         */
index 6825c7b..5cc9a96 100644 (file)
@@ -361,12 +361,6 @@ foreach ( $wgForeignFileRepos as &$repo ) {
 }
 unset( $repo ); // no global pollution; destroy reference
 
-// Convert this deprecated setting to modern system
-if ( $wgExperimentalHtmlIds ) {
-       wfDeprecated( '$wgExperimentalHtmlIds', '1.30' );
-       $wgFragmentMode = [ 'html5-legacy', 'html5' ];
-}
-
 $rcMaxAgeDays = $wgRCMaxAge / ( 3600 * 24 );
 if ( $wgRCFilterByAge ) {
        // Trim down $wgRCLinkDays so that it only lists links which are valid
index 0816ed7..5be4703 100644 (file)
        "apihelp-edit-param-tags": "Cambia las etiquetas para aplicarlas a la revisión.",
        "apihelp-edit-param-minor": "Edición menor.",
        "apihelp-edit-param-notminor": "Edición no menor.",
-       "apihelp-edit-param-bot": "Marcar esta edición como edición de bot.",
+       "apihelp-edit-param-bot": "Marcar esta como una edición de robot.",
        "apihelp-edit-param-basetimestamp": "Marca de tiempo de la revisión base, usada para detectar conflictos de edición. Se puede obtener mediante [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]",
        "apihelp-edit-param-starttimestamp": "Marca de tiempo de cuando empezó el proceso de edición, usada para detectar conflictos de edición. Se puede obtener un valor apropiado usando <var>[[Special:ApiHelp/main|curtimestamp]]</var> cuando comiences el proceso de edición (por ejemplo, al cargar el contenido de la página por editar).",
        "apihelp-edit-param-recreate": "Reemplazar los errores acerca de la página de haber sido eliminados en el ínterin.",
index 9b25d53..8543c4b 100644 (file)
@@ -36,11 +36,14 @@ use Wikimedia\Rdbms\LoadBalancer;
  * Updates that work through this system will be more likely to complete by the time the client
  * makes their next request after this one than with the JobQueue system.
  *
- * In CLI mode, updates run immediately if no DB writes are pending. Otherwise, they run when:
- *   - a) Any waitForReplication() call if no writes are pending on any DB
- *   - b) A commit happens on Maintenance::getDB( DB_MASTER ) if no writes are pending on any DB
- *   - c) EnqueueableDataUpdate tasks may enqueue on commit of Maintenance::getDB( DB_MASTER )
- *   - d) At the completion of Maintenance::execute()
+ * In CLI mode, deferred updates will run:
+ *   - a) During DeferredUpdates::addUpdate if no LBFactory DB handles have writes pending
+ *   - b) On commit of an LBFactory DB handle if no other such handles have writes pending
+ *   - c) During an LBFactory::waitForReplication call if no LBFactory DBs have writes pending
+ *   - d) When the queue is large and an LBFactory DB handle commits (EnqueueableDataUpdate only)
+ *   - e) At the completion of Maintenance::execute()
+ *
+ * @see Maintenance::setLBFactoryTriggers
  *
  * When updates are deferred, they go into one two FIFO "top-queues" (one for pre-send and one
  * for post-send). Updates enqueued *during* doUpdate() of a "top" update go into the "sub-queue"
@@ -206,23 +209,29 @@ class DeferredUpdates {
                        foreach ( $updatesByType as $updatesForType ) {
                                foreach ( $updatesForType as $update ) {
                                        self::$executeContext = [ 'stage' => $stage, 'subqueue' => [] ];
-                                       /** @var DeferrableUpdate $update */
-                                       $guiError = self::runUpdate( $update, $lbFactory, $mode, $stage );
-                                       $reportableError = $reportableError ?: $guiError;
-                                       // Do the subqueue updates for $update until there are none
-                                       while ( self::$executeContext['subqueue'] ) {
-                                               $subUpdate = reset( self::$executeContext['subqueue'] );
-                                               $firstKey = key( self::$executeContext['subqueue'] );
-                                               unset( self::$executeContext['subqueue'][$firstKey] );
-
-                                               if ( $subUpdate instanceof DataUpdate ) {
-                                                       $subUpdate->setTransactionTicket( $ticket );
-                                               }
-
-                                               $guiError = self::runUpdate( $subUpdate, $lbFactory, $mode, $stage );
+                                       try {
+                                               /** @var DeferrableUpdate $update */
+                                               $guiError = self::runUpdate( $update, $lbFactory, $mode, $stage );
                                                $reportableError = $reportableError ?: $guiError;
+                                               // Do the subqueue updates for $update until there are none
+                                               while ( self::$executeContext['subqueue'] ) {
+                                                       $subUpdate = reset( self::$executeContext['subqueue'] );
+                                                       $firstKey = key( self::$executeContext['subqueue'] );
+                                                       unset( self::$executeContext['subqueue'][$firstKey] );
+
+                                                       if ( $subUpdate instanceof DataUpdate ) {
+                                                               $subUpdate->setTransactionTicket( $ticket );
+                                                       }
+
+                                                       $guiError = self::runUpdate( $subUpdate, $lbFactory, $mode, $stage );
+                                                       $reportableError = $reportableError ?: $guiError;
+                                               }
+                                       } finally {
+                                               // Make sure we always clean up the context.
+                                               // Losing updates while rewinding the stack is acceptable,
+                                               // losing updates that are added later is not.
+                                               self::$executeContext = null;
                                        }
-                                       self::$executeContext = null;
                                }
                        }
 
@@ -265,6 +274,12 @@ class DeferredUpdates {
                                $guiError = $e;
                        }
                        MWExceptionHandler::rollbackMasterChangesAndLog( $e );
+
+                       // VW-style hack to work around T190178, so we can make sure
+                       // PageMetaDataUpdater doesn't throw exceptions.
+                       if ( defined( 'MW_PHPUNIT_TEST' ) ) {
+                               throw $e;
+                       }
                }
 
                return $guiError;
@@ -273,8 +288,9 @@ class DeferredUpdates {
        /**
         * Run all deferred updates immediately if there are no DB writes active
         *
-        * If $mode is 'run' but there are busy databates, EnqueueableDataUpdate
-        * tasks will be enqueued anyway for the sake of progress.
+        * If there are many deferred updates pending, $mode is 'run', and there
+        * are still busy LBFactory database handles, then any EnqueueableDataUpdate
+        * tasks might be enqueued as jobs to be executed later.
         *
         * @param string $mode Use "enqueue" to use the job queue when possible
         * @return bool Whether updates were allowed to run
@@ -361,7 +377,7 @@ class DeferredUpdates {
         */
        private static function areDatabaseTransactionsActive() {
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
-               if ( $lbFactory->hasTransactionRound() ) {
+               if ( $lbFactory->hasTransactionRound() || !$lbFactory->isReadyForRoundOperations() ) {
                        return true;
                }
 
index 8913642..4ddd151 100644 (file)
@@ -177,15 +177,16 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
 
                // Commit and release the lock (if set)
                ScopedCallback::consume( $scopedLock );
-               // Run post-commit hooks without DBO_TRX
-               $this->getDB()->onTransactionIdle(
+               // Run post-commit hook handlers without DBO_TRX
+               DeferredUpdates::addUpdate( new AutoCommitUpdate(
+                       $this->getDB(),
+                       __METHOD__,
                        function () {
                                // Avoid PHP 7.1 warning from passing $this by reference
                                $linksUpdate = $this;
                                Hooks::run( 'LinksUpdateComplete', [ &$linksUpdate, $this->ticket ] );
-                       },
-                       __METHOD__
-               );
+                       }
+               ) );
        }
 
        /**
index e94c71b..18c917e 100644 (file)
@@ -64,7 +64,7 @@
        "config-apc": "[http://www.php.net/apc APC] instalatuta dago",
        "config-apcu": "[http://www.php.net/apcu APCu] instalatuta dago",
        "config-wincache": "[https://www.iis.net/download/WinCacheForPhp WinCache] instalatuta dago",
-       "config-no-cache-apcu": "<strong>Warning:</strong> Ezin izan da [http://www.php.net/apcu APCu], [http://xcache.lighttpd.net/ XCache] edo [http://www.iis.net/download/WinCacheForPhp WinCache] aurkitu.\nObjektu katxea ez dago aktibatuta.",
+       "config-no-cache-apcu": "<strong>Warning:</strong> Ezin izan da [http://www.php.net/apcu APCu] edo [http://www.iis.net/download/WinCacheForPhp WinCache] aurkitu.\nObjektu katxea ez dago aktibatuta.",
        "config-mod-security": "<strong>Warning:</strong> Zure web zerbitzariak [https://modsecurity.org/mod_security] / mod_security2 aktibatu du. Honen konfigurazio komun asko sortu ahal dituzte arazoak MediaWikin  eta beste software batzuetan, hautazko edukia argitaratzeko aukera ematen dutenei erabiltzaileei.\nAhal izanez gero, desgaitu egin beharko litzateke. Bestela, kontsultatu [https://modsecurity.org/documentation/ mod_security documentation] edo jarri harremanetan zure ostalariarekin ausazko akatsak aurkitzen badituzu.",
        "config-diff3-bad": "GNU diff3 ez da aurkitu.",
        "config-git": "Git bertsio-kontrol software aurkitu da: <code>$1</code>",
        "config-cache-options": "Objektu cachearen ezarpenak:",
        "config-cache-help": "Objektuen katxea erabiltzen da MediaWikiko abiadura hobetzeko, sarritan erabiltzen diren datuak gordetzen.\nOso gomendagarria da, webgune handientzako eta ertainentzako, webgune txikiek ere ikusiko dituzte onurak.",
        "config-cache-none": "Desaktibatu Katxina (ez dira funtzionaltasunak ezabatu, baina wiki orrialde handietan abiaduran eragina izan ahal du)",
-       "config-cache-accel": "PHP objetuen katxea (APC, APCu, XCache edo WinCache)",
+       "config-cache-accel": "PHP objetuen katxea (APC, APCu, edo WinCache)",
        "config-cache-memcached": "Memcached erabili (konfigurazio eta instalazio gehiago behar du)",
        "config-memcached-servers": "Memcached serbidoreak:",
        "config-memcached-help": "Memcached-ekin erabiltzeko IP helbideen lista.\nLerro bakoitzen bat bakarrik jarri behar da eta zehaztu ze ataka erabiliko den. Adibidez:\n127.0.0.1:11211\n192.168.1.25:1234",
        "config-nofile": "Ezin da \"$1\" fitxategia aurkitu. Ezabatua izan da?",
        "config-extension-link": "Ba al zenekien wikiak [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions] onartzen dituela?\n\nArakatu [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category] edo [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] ikusi ahal izateko luzapenen zerrenda.",
        "config-skins-screenshots": "$1 (Pantaila-irudia: $2)",
+       "config-extensions-requires": "$1 ($2 behar du)",
        "config-screenshot": "Pantaila-irudia",
        "mainpagetext": "<strong>MediaWiki instalatu da.</strong>",
        "mainpagedocfooter": "Ikusi [https://meta.wikimedia.org/wiki/Help:Contents Erabiltzailearen Gida] wiki softwarea erabiltzen hasteko informazio gehiagorako.\n\n== Nola hasi ==\n\n*\n [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Konfigurazio balioen zerrenda]\n*\n [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ (MediaWikin Maiz egindako galderak)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiren argitalpenen posta zerrenda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Aurkitu MediaWiki zure hizkuntzan]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Zure wikian spam-a nola borrokatzen ikasi]"
index 55ce440..4f158f9 100644 (file)
@@ -56,6 +56,7 @@
        "config-connection-error": "$1.\n\nതാഴെ നൽകിയിരിക്കുന്ന ഹോസ്റ്റ്, ഉപയോക്തൃനാമം, രഹസ്യവാക്ക് എന്നിവ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക.",
        "config-regenerate": "LocalSettings.php പുനഃസൃഷ്ടിക്കുക →",
        "config-mysql-engine": "സ്റ്റോറേജ് എൻജിൻ:",
+       "config-mysql-utf8": "യു.ടി.എഫ്.-8",
        "config-site-name": "വിക്കിയുടെ പേര്:",
        "config-site-name-help": "ഇത് ബ്രൗസറിന്റെ ടൈറ്റിൽ ബാറിലും മറ്റനേകം ഇടങ്ങളിലും പ്രദർശിപ്പിക്കപ്പെടും.",
        "config-site-name-blank": "സൈറ്റിന്റെ പേര് നൽകുക.",
index 1e8838e..45e7cbb 100644 (file)
@@ -195,12 +195,22 @@ interface ILBFactory {
        public function rollbackMasterChanges( $fname = __METHOD__ );
 
        /**
-        * Check if a transaction round is active
+        * Check if an explicit transaction round is active
         * @return bool
         * @since 1.29
         */
        public function hasTransactionRound();
 
+       /**
+        * Check if transaction rounds can be started, committed, or rolled back right now
+        *
+        * This can be used as a recusion guard to avoid exceptions in transaction callbacks
+        *
+        * @return bool
+        * @since 1.32
+        */
+       public function isReadyForRoundOperations();
+
        /**
         * Determine if any master connection has pending changes
         * @return bool
index ca684c3..ccaebd3 100644 (file)
@@ -287,6 +287,10 @@ abstract class LBFactory implements ILBFactory {
                return ( $this->trxRoundId !== false );
        }
 
+       public function isReadyForRoundOperations() {
+               return ( $this->trxRoundStage === self::ROUND_CURSORY );
+       }
+
        /**
         * Log query info if multi DB transactions are going to be committed now
         */
index 24fdfb0..c047e96 100644 (file)
@@ -65,10 +65,11 @@ class LogPager extends ReverseChronologicalPager {
         * @param int|bool $month The month to start from. Default: false
         * @param string $tagFilter Tag
         * @param string $action Specific action (subtype) requested
+        * @param int $logId Log entry ID, to limit to a single log entry.
         */
        public function __construct( $list, $types = [], $performer = '', $title = '',
                $pattern = '', $conds = [], $year = false, $month = false, $tagFilter = '',
-               $action = ''
+               $action = '', $logId = false
        ) {
                parent::__construct( $list->getContext() );
                $this->mConds = $conds;
@@ -81,6 +82,7 @@ class LogPager extends ReverseChronologicalPager {
                $this->limitAction( $action );
                $this->getDateCond( $year, $month );
                $this->mTagFilter = $tagFilter;
+               $this->limitLogId( $logId );
 
                $this->mDb = wfGetDB( DB_REPLICA, 'logpager' );
        }
@@ -278,6 +280,17 @@ class LogPager extends ReverseChronologicalPager {
                }
        }
 
+       /**
+        * Limit to the (single) specified log ID.
+        * @param int $logId The log entry ID.
+        */
+       protected function limitLogId( $logId ) {
+               if ( !$logId ) {
+                       return;
+               }
+               $this->mConds['log_id'] = $logId;
+       }
+
        /**
         * Constructs the most part of the query. Extra conditions are sprinkled in
         * all over this class.
index b13e597..118442d 100644 (file)
@@ -1180,13 +1180,12 @@ class Sanitizer {
 
        /**
         * Given a value, escape it so that it can be used in an id attribute and
-        * return it.  This will use HTML5 validation if $wgExperimentalHtmlIds is
-        * true, allowing anything but ASCII whitespace.  Otherwise it will use
-        * HTML 4 rules, which means a narrow subset of ASCII, with bad characters
-        * escaped with lots of dots.
+        * return it.  This will use HTML5 validation, allowing anything but ASCII
+        * whitespace.
+        *
+        * To ensure we don't have to bother escaping anything, we also strip ', ".
+        * TODO: Is this the best tactic?
         *
-        * To ensure we don't have to bother escaping anything, we also strip ', ",
-        * & even if $wgExperimentalIds is true.  TODO: Is this the best tactic?
         * We also strip # because it upsets IE, and % because it could be
         * ambiguous if it's part of something that looks like a percent escape
         * (which don't work reliably in fragments cross-browser).
@@ -1204,28 +1203,12 @@ class Sanitizer {
         * @param string|array $options String or array of strings (default is array()):
         *   'noninitial': This is a non-initial fragment of an id, not a full id,
         *       so don't pay attention if the first character isn't valid at the
-        *       beginning of an id.  Only matters if $wgExperimentalHtmlIds is
-        *       false.
-        *   'legacy': Behave the way the old HTML 4-based ID escaping worked even
-        *       if $wgExperimentalHtmlIds is used, so we can generate extra
-        *       anchors and links won't break.
+        *       beginning of an id.
         * @return string
         */
        static function escapeId( $id, $options = [] ) {
-               global $wgExperimentalHtmlIds;
                $options = (array)$options;
 
-               if ( $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) {
-                       $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
-                       $id = trim( $id, '_' );
-                       if ( $id === '' ) {
-                               // Must have been all whitespace to start with.
-                               return '_';
-                       } else {
-                               return $id;
-                       }
-               }
-
                // HTML4-style escaping
                static $replace = [
                        '%3A' => ':',
@@ -1337,14 +1320,6 @@ class Sanitizer {
                                $id = urlencode( str_replace( ' ', '_', $id ) );
                                $id = strtr( $id, $replace );
                                break;
-                       case 'html5-legacy':
-                               $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
-                               $id = trim( $id, '_' );
-                               if ( $id === '' ) {
-                                       // Must have been all whitespace to start with.
-                                       $id = '_';
-                               }
-                               break;
                        default:
                                throw new InvalidArgumentException( "Invalid mode '$mode' passed to '" . __METHOD__ );
                }
index fbd0a24..de25d32 100644 (file)
@@ -93,6 +93,9 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule {
        }
 
        /**
+        * Non-static proxy to ::getLogo (for overloading in sub classes or tests).
+        *
+        * @codeCoverageIgnore
         * @since 1.31
         * @param Config $conf
         * @return string|array
index 9c4ac50..2247cc2 100644 (file)
@@ -179,7 +179,9 @@ abstract class Skin extends ContextSource {
                        // Styles key sets render blocking styles
                        // Unlike other keys in this definition it is an associative array
                        // where each key is the group name and points to a list of modules
-                       'styles' => [],
+                       'styles' => [
+                               'content' => [],
+                       ],
                        // modules not specific to any specific skin or page
                        'core' => [
                                // Enforce various default modules for all pages and all skins
@@ -208,11 +210,13 @@ abstract class Skin extends ContextSource {
                // Preload jquery.tablesorter for mediawiki.page.ready
                if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
                        $modules['content'][] = 'jquery.tablesorter';
+                       $modules['styles']['content'][] = 'jquery.tablesorter.styles';
                }
 
                // Preload jquery.makeCollapsible for mediawiki.page.ready
                if ( strpos( $out->getHTML(), 'mw-collapsible' ) !== false ) {
                        $modules['content'][] = 'jquery.makeCollapsible';
+                       $modules['styles']['content'][] = 'jquery.makeCollapsible.styles';
                }
 
                if ( $out->isTOCEnabled() ) {
index 4587533..9461bcf 100644 (file)
@@ -65,7 +65,6 @@ class SkinTemplate extends Skin {
                $moduleStyles = [
                        'mediawiki.legacy.shared',
                        'mediawiki.legacy.commonPrint',
-                       'mediawiki.sectionAnchor'
                ];
                if ( $out->isSyndicated() ) {
                        $moduleStyles[] = 'mediawiki.feedlink';
index 6a11bf4..bad1746 100644 (file)
@@ -51,6 +51,7 @@ class SpecialLog extends SpecialPage {
                $opts->add( 'dir', '' );
                $opts->add( 'offender', '' );
                $opts->add( 'subtype', '' );
+               $opts->add( 'logid', '' );
 
                // Set values
                $opts->fetchValuesFromRequest( $this->getRequest() );
@@ -169,6 +170,16 @@ class SpecialLog extends SpecialPage {
                return $subpages;
        }
 
+       /**
+        * Set options based on the subpage title parts:
+        * - One part that is a valid log type: Special:Log/logtype
+        * - Two parts: Special:Log/logtype/username
+        * - Otherwise, assume the whole subpage is a username.
+        *
+        * @param FormOptions $opts
+        * @param $par
+        * @throws ConfigException
+        */
        private function parseParams( FormOptions $opts, $par ) {
                # Get parameters
                $par = $par !== null ? $par : '';
@@ -204,7 +215,8 @@ class SpecialLog extends SpecialPage {
                        $opts->getValue( 'year' ),
                        $opts->getValue( 'month' ),
                        $opts->getValue( 'tagfilter' ),
-                       $opts->getValue( 'subtype' )
+                       $opts->getValue( 'subtype' ),
+                       $opts->getValue( 'logid' )
                );
 
                $this->addHeader( $opts->getValue( 'type' ) );
index 36e7779..e827911 100644 (file)
@@ -162,7 +162,7 @@ class SpecialRedirect extends FormSpecialPage {
 
        /**
         * Handle Special:Redirect/logid/xxx
-        * (by redirecting to index.php?title=Special:Log)
+        * (by redirecting to index.php?title=Special:Log&logid=xxx)
         *
         * @since 1.27
         * @return string|null Url to redirect to, or null if $mValue is invalid.
@@ -176,80 +176,8 @@ class SpecialRedirect extends FormSpecialPage {
                if ( $logid === 0 ) {
                        return null;
                }
-
-               $logQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
-
-               $logparams = [
-                       'log_id' => 'log_id',
-                       'log_timestamp' => 'log_timestamp',
-                       'log_type' => 'log_type',
-                       'log_user_text' => $logQuery['fields']['log_user_text'],
-               ];
-
-               $dbr = wfGetDB( DB_REPLICA );
-
-               // Gets the nested SQL statement which
-               // returns timestamp of the log with the given log ID
-               $inner = $dbr->selectSQLText(
-                       'logging',
-                       [ 'log_timestamp' ],
-                       [ 'log_id' => $logid ]
-               );
-
-               // Returns all fields mentioned in $logparams of the logs
-               // with the same timestamp as the one returned by the statement above
-               $logsSameTimestamps = $dbr->select(
-                       [ 'logging' ] + $logQuery['tables'],
-                       $logparams,
-                       [ "log_timestamp = ($inner)" ],
-                       __METHOD__,
-                       [],
-                       $logQuery['joins']
-               );
-               if ( $logsSameTimestamps->numRows() === 0 ) {
-                       return null;
-               }
-
-               // Stores the row with the same log ID as the one given
-               $rowMain = [];
-               foreach ( $logsSameTimestamps as $row ) {
-                       if ( (int)$row->log_id === $logid ) {
-                               $rowMain = $row;
-                       }
-               }
-
-               array_shift( $logparams );
-
-               // Stores all the rows with the same values in each column
-               // as $rowMain
-               foreach ( $logparams as $key => $dummy ) {
-                       $matchedRows = [];
-                       foreach ( $logsSameTimestamps as $row ) {
-                               if ( $row->$key === $rowMain->$key ) {
-                                       $matchedRows[] = $row;
-                               }
-                       }
-                       if ( count( $matchedRows ) === 1 ) {
-                               break;
-                       }
-                       $logsSameTimestamps = $matchedRows;
-               }
-               $query = [ 'title' => 'Special:Log', 'limit' => count( $matchedRows ) ];
-
-               // A map of database field names from table 'logging' to the values of $logparams
-               $keys = [
-                       'log_timestamp' => 'offset',
-                       'log_type' => 'type',
-                       'log_user_text' => 'user'
-               ];
-
-               foreach ( $logparams as $logKey => $dummy ) {
-                       $query[$keys[$logKey]] = $matchedRows[0]->$logKey;
-               }
-               $query['offset'] = $query['offset'] + 1;
-               $url = $query;
-
-               return wfAppendQuery( wfScript( 'index' ), $url );
+               $query = [ 'title' => 'Special:Log', 'logid' => $logid ];
+               return wfAppendQuery( wfScript( 'index' ), $query );
        }
 
        /**
index 723093a..b60882a 100644 (file)
@@ -19,9 +19,9 @@
  */
 
 /**
- * Extend HTMLForm purely so we can have a more sane way of getting the section headers
+ * Extend OOUIHTMLForm purely so we can have a more sane way of getting the section headers
  */
-class EditWatchlistNormalHTMLForm extends HTMLForm {
+class EditWatchlistNormalHTMLForm extends OOUIHTMLForm {
        public function getLegend( $namespace ) {
                $namespace = substr( $namespace, 2 );
 
@@ -29,8 +29,4 @@ class EditWatchlistNormalHTMLForm extends HTMLForm {
                        ? $this->msg( 'blanknamespace' )->escaped()
                        : htmlspecialchars( $this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
        }
-
-       public function getBody() {
-               return $this->displaySection( $this->mFieldTree, '', 'editwatchlist-' );
-       }
 }
index 01a5a79..1698b9f 100644 (file)
  */
 class CrhConverter extends LanguageConverter {
        // Defines working character ranges
-       const WORD_BEGINS = '\r\s\"\'\(\)\-<>\[\]\/.,:;!?';
-       const WORD_ENDS = '\r\s\"\'\(\)\-<>\[\]\/.,:;!?';
 
        // Cyrillic
        const C_UC = 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'; # Crimean Tatar Cyrillic uppercase
        const C_LC = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'; # Crimean Tatar Cyrillic lowercase
        const C_CONS_UC = 'БВГДЖЗЙКЛМНПРСТФХЦЧШЩCÑ'; # Crimean Tatar Cyrillic + CÑ uppercase consonants
        const C_CONS_LC = 'бвгджзйклмнпрстфхцчшщcñ'; # Crimean Tatar Cyrillic + CÑ lowercase consonants
-       const C_M_CONS = 'бгкмпшcБГКМПШC'; # Crimean Tatar Cyrillic M-type consonants
+       const C_M_CONS = 'бгкмшcБГКМШC'; # Crimean Tatar Cyrillic M-type consonants
 
-       # Crimean Tatar Cyrillic + CÑ consonants
+       // Crimean Tatar Cyrillic + CÑ consonants
        const C_CONS = 'бвгджзйклмнпрстфхцчшщcñБВГДЖЗЙКЛМНПРСТФХЦЧШЩCÑ';
 
        // Latin
@@ -50,9 +48,9 @@ class CrhConverter extends LanguageConverter {
        const L_N_CONS_LC = 'çnrstz'; # Crimean Tatar Latin N-type lower case consonants
        const L_N_CONS = 'çnrstzÇNRSTZ'; # Crimean Tatar Latin N-type consonants
        const L_M_CONS = 'bcgkmpşBCGKMPŞ'; # Crimean Tatar Latin M-type consonants
-       const L_CONS_UC = 'BCÇDFGHJKLMNÑPRSŞTVZ'; # Crimean Tatar Latin uppercase consonants
-       const L_CONS_LC = 'bcçdfghjklmnñprsştvz'; # Crimean Tatar Latin lowercase consonants
-       const L_CONS = 'bcçdfghjklmnñprsştvzBCÇDFGHJKLMNÑPRSŞTVZ'; # Crimean Tatar Latin consonants
+       const L_CONS_UC = 'BCÇDFGĞHJKLMNÑPQRSŞTVZ'; # Crimean Tatar Latin uppercase consonants
+       const L_CONS_LC = 'bcçdfgğhjklmnñpqrsştvz'; # Crimean Tatar Latin lowercase consonants
+       const L_CONS = 'bcçdfgğhjklmnñpqrsştvzBCÇDFGĞHJKLMNÑPQRSŞTVZ'; # Crimean Tatar Latin consonants
        const L_VOW_UC = 'AÂEIİOÖUÜ'; # Crimean Tatar Latin uppercase vowels
        const L_VOW = 'aâeıioöuüAÂEIİOÖUÜ'; # Crimean Tatar Latin vowels
        const L_F_UC = 'EİÖÜ'; # Crimean Tatar Latin uppercase front vowels
@@ -133,9 +131,12 @@ class CrhConverter extends LanguageConverter {
 
                ];
 
-       public $mExceptions = [];
+       public $mCyrl2LatnExceptions = [];
+       public $mLatn2CyrlExceptions = [];
+
        public $mCyrl2LatnPatterns = [];
        public $mLatn2CyrlPatterns = [];
+
        public $mCyrlCleanUpRegexes = [];
 
        public $mExceptionsLoaded = false;
@@ -155,9 +156,9 @@ class CrhConverter extends LanguageConverter {
 
                $this->mExceptionsLoaded = true;
                $crhExceptions = new MediaWiki\Languages\Data\CrhExceptions();
-               list( $this->mExceptions, $this->mCyrl2LatnPatterns, $this->mLatn2CyrlPatterns,
-                       $this->mCyrlCleanUpRegexes ) = $crhExceptions->loadExceptions( self::L_LC . self::C_LC,
-                       self::L_UC . self::C_UC );
+               list( $this->mCyrl2LatnExceptions, $this->mLatn2CyrlExceptions,
+                       $this->mCyrl2LatnPatterns, $this->mLatn2CyrlPatterns, $this->mCyrlCleanUpRegexes ) =
+                       $crhExceptions->loadExceptions( self::L_LC . self::C_LC, self::L_UC . self::C_UC );
        }
 
        /**
@@ -197,17 +198,12 @@ class CrhConverter extends LanguageConverter {
         * @return string
         */
        function translate( $text, $toVariant ) {
-               $letters = '';
                switch ( $toVariant ) {
                        case 'crh-cyrl':
-                               $letters = self::L_UC . self::L_LC . "\'";
-                               break;
                        case 'crh-latn':
-                               $letters = self::C_UC . self::C_LC . "";
                                break;
                        default:
                                return $text;
-                               break;
                }
 
                if ( !$this->mTablesLoaded ) {
@@ -218,48 +214,41 @@ class CrhConverter extends LanguageConverter {
                        throw new MWException( "Broken variant table: " . implode( ',', array_keys( $this->mTables ) ) );
                }
 
-               // check for roman numbers like VII, XIX...
-               // Lookahead assertion ensures $roman doesn't match the empty string
-               $roman = '/^(?=[MDCLXVI])M{0,4}(C[DM]|D?C{0,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3})$/u';
-
-               # match any sub-string of the relevant letters and convert it
-               $matches = preg_split( '/(\b|^)[^' . $letters . ']+(\b|$)/u',
-                       $text, -1, PREG_SPLIT_OFFSET_CAPTURE );
-               $mstart = 0;
-               $ret = '';
-               foreach ( $matches as $m ) {
-                       # copy over the non-matching bit
-                       $ret .= substr( $text, $mstart, $m[1] - $mstart );
-                       # skip certain classes of strings
-
-                       if ( array_key_exists( $m[0], $this->mExceptions ) ) {
-                               # if it's an exception, just copy down the right answer
-                               $ret .= $this->mExceptions[$m[0]];
-                       } elseif ( ! $m[0] || # empty strings
-                                        preg_match( $roman, $m[0] ) || # roman numerals
-                                        preg_match( '/[^' . $letters . ']/', $m[0] ) # mixed orthography
-                                       ) {
-                               $ret .= $m[0];
-                       } else {
-                               # convert according to the rules
-                               $token = $this->regsConverter( $m[0], $toVariant );
-                               $ret .= parent::translate( $token, $toVariant );
-                       }
-                       $mstart = $m[1] + strlen( $m[0] );
-               }
-
-               # pick up stray quote marks
                switch ( $toVariant ) {
                        case 'crh-cyrl':
-                               $ret = strtr( $ret, [ '“' => '«', '”' => '»', ] );
-                               $ret = $this->regsConverter( $ret, 'cyrl-cleanup' );
-                               break;
-                       case 'crh-latn':
-                               $ret = strtr( $ret, [ '«' => '"', '»' => '"', ] );
-                               break;
-               }
+                               /* Check for roman numbers like VII, XIX...
+                                * Only need to split on Roman numerals when converting to Cyrillic
+                                * Lookahead assertion ensures $roman doesn't match the empty string, and
+                                * non-period after first "Roman" character allows initials to be converted
+                                */
+                               $roman = '(?=[MDCLXVI]([^.]|$))M{0,4}(C[DM]|D?C{0,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3})';
+
+                               $breaks = '([^\w\x80-\xff])';
+
+                               // allow for multiple Roman numerals in a row; rare but it happens
+                               $romanRegex = '/^' . $roman . '$|^(' . $roman . $breaks . ')+|(' . $breaks . $roman . ')+$|' .
+                                       $breaks . '(' . $roman . $breaks . ')+/';
+
+                               $matches = preg_split( $romanRegex, $text, -1, PREG_SPLIT_OFFSET_CAPTURE );
+                               $mstart = 0;
+                               $ret = '';
+                               foreach ( $matches as $m ) {
+                                       // copy over Roman numerals
+                                       $ret .= substr( $text, $mstart, $m[1] - $mstart );
+
+                                       // process everything else
+                                       if ( $m[0] !== '' ) {
+                                               $ret .= $this->regsConverter( $m[0], $toVariant );
+                                       }
+
+                                       $mstart = $m[1] + strlen( $m[0] );
+                               }
 
-               return $ret;
+                               return $ret;
+                       default:
+                               // Just process the whole string in one go
+                               return $this->regsConverter( $text, $toVariant );
+               }
        }
 
        private function regsConverter( $text, $toVariant ) {
@@ -269,16 +258,20 @@ class CrhConverter extends LanguageConverter {
                $rep = [];
                switch ( $toVariant ) {
                        case 'crh-latn':
+                               $text = strtr( $text, $this->mCyrl2LatnExceptions );
                                foreach ( $this->mCyrl2LatnPatterns as $pat => $rep ) {
                                        $text = preg_replace( $pat, $rep, $text );
                                }
+                               $text = parent::translate( $text, $toVariant );
+                               $text = strtr( $text, [ '«' => '"', '»' => '"', ] );
                                return $text;
                        case 'crh-cyrl':
+                               $text = strtr( $text, $this->mLatn2CyrlExceptions );
                                foreach ( $this->mLatn2CyrlPatterns as $pat => $rep ) {
                                        $text = preg_replace( $pat, $rep, $text );
                                }
-                               return $text;
-                       case 'cyrl-cleanup':
+                               $text = parent::translate( $text, $toVariant );
+                               $text = strtr( $text, [ '“' => '«', '”' => '»', ] );
                                foreach ( $this->mCyrlCleanUpRegexes as $pat => $rep ) {
                                        $text = preg_replace( $pat, $rep, $text );
                                }
index d656528..e3bb156 100644 (file)
@@ -17,7 +17,9 @@ class CrhExceptions {
                $this->loadRegs();
        }
 
-       public $exceptionMap = [];
+       public $Cyrl2LatnExceptions = [];
+       public $Latn2CyrlExceptions = [];
+
        public $Cyrl2LatnPatterns = [];
        public $Latn2CyrlPatterns = [];
 
@@ -59,10 +61,12 @@ class CrhExceptions {
        private function addMappings( $mapArray, &$A2B, &$B2A, $exactCase = false,
                        $prePat = '', $postPat = '' ) {
                foreach ( $mapArray as $WordA => $WordB ) {
-                       $ucA = $this->myUc( $WordA );
-                       $ucWordA = $this->myUcWord( $WordA );
-                       $ucB = $this->myUc( $WordB );
-                       $ucWordB = $this->myUcWord( $WordB );
+                       if ( ! $exactCase ) {
+                               $ucA = $this->myUc( $WordA );
+                               $ucWordA = $this->myUcWord( $WordA );
+                               $ucB = $this->myUc( $WordB );
+                               $ucWordB = $this->myUcWord( $WordB );
+                       }
 
                        # if there are regexes, only map toward backregs
                        if ( ! preg_match( '/\$[1-9]/', $WordA ) ) {
@@ -86,94 +90,130 @@ class CrhExceptions {
        function loadExceptions( $lcChars, $ucChars ) {
                # init lc and uc, as needed
                $this->initLcUc( $lcChars, $ucChars );
-               # load C2L and L2C whole-word exceptions into the same array, since it's just a look up
-               # no regex prefix/suffix needed
-               $this->addMappings( $this->wordMappings, $this->exceptionMap, $this->exceptionMap );
-               $this->addMappings( $this->exactCaseMappings, $this->exceptionMap, $this->exceptionMap, true );
 
-               # load C2L and L2C bidirectional prefix mappings
+               # no regex prefix/suffix needed
+               $this->addMappings( $this->ManyToOneC2LMappings,
+                       // reverse exception mapping order to handle many-to-one C2L mappings
+                       $this->Latn2CyrlExceptions, $this->Cyrl2LatnExceptions );
+               $this->addMappings( $this->multiCaseMappings,
+                       $this->Cyrl2LatnExceptions, $this->Latn2CyrlExceptions );
+               $this->addMappings( $this->exactCaseMappings,
+                       $this->Cyrl2LatnExceptions, $this->Latn2CyrlExceptions, true );
+
+               # load C2L and L2C bidirectional affix mappings
                $this->addMappings( $this->prefixMapping,
-                       $this->Cyrl2LatnPatterns, $this->Latn2CyrlPatterns, false, '/^', '/u' );
+                       $this->Cyrl2LatnPatterns, $this->Latn2CyrlPatterns, false, '/\b', '/u' );
                $this->addMappings( $this->suffixMapping,
-                       $this->Cyrl2LatnPatterns, $this->Latn2CyrlPatterns, false, '/', '$/u' );
+                       $this->Cyrl2LatnPatterns, $this->Latn2CyrlPatterns, false, '/', '\b/u' );
 
                # tack on one-way mappings to the ends of the prefix and suffix patterns
                $this->Cyrl2LatnPatterns += $this->Cyrl2LatnRegexes;
                $this->Latn2CyrlPatterns += $this->Latn2CyrlRegexes;
 
-               return [ $this->exceptionMap, $this->Cyrl2LatnPatterns,
+               return [ $this->Cyrl2LatnExceptions, $this->Latn2CyrlExceptions, $this->Cyrl2LatnPatterns,
                        $this->Latn2CyrlPatterns, $this->CyrlCleanUpRegexes ];
        }
 
-       # map Cyrillic to Latin and back, whole word match only
+       # map Latin to Cyrillic and back, simple string match only (no regex)
        # variants: all lowercase, all uppercase, first letter capitalized
-       # items with capture group refs (e.g., $1) are only mapped from the
-       # regex to the reference
-       private $wordMappings = [
+       private $ManyToOneC2LMappings = [
+               # Carefully ordered many-to-one mappings
+               # these are ordered so C2L is correct (the later Latin one)
+               # see also L2C mappings below
+               'fevqülade' => 'февкъульаде', 'fevqulade' => 'февкъульаде',
+               'beyude' => 'бейуде', 'beyüde' => 'бейуде',
+               'curat' => 'джурьат', 'cürat' => 'джурьат',
+               'mesul' => 'месуль', 'mesül' => 'месуль',
+       ];
+
+       # map Cyrillic to Latin and back, simple string match only (no regex)
+       # variants: all lowercase, all uppercase, first letter capitalized
+       private $multiCaseMappings = [
 
-               #### originally Cyrillic to Latin
+               #### Cyrillic to Latin
                'аджыумер' => 'acıümer', 'аджыусеин' => 'acıüsein', 'алейкум' => 'aleyküm',
-               'бейуде' => 'beyüde', 'боливия' => 'boliviya', 'большевик' => 'bolşevik', 'борис' => 'boris',
-               'борнен' => 'bornen', 'бугун' => 'bugün', 'бузкесен' => 'buzkesen', 'буксир' => 'buksir',
-               'бульбуль' => 'bülbül', 'бульвар' => 'bulvar', 'бульдозер' => 'buldozer', 'бульон' => 'bulyon',
+               'бозтюс' => 'boztüs', 'боливия' => 'boliviya', 'большевик' => 'bolşevik', 'борис' => 'boris',
+               'борнен' => 'bornen', 'бублик' => 'bublik', 'буддизм' => 'buddizm', 'буддист' => 'buddist',
+               'буженина' => 'bujenina', 'бузкесен' => 'buzkesen', 'букинист' => 'bukinist',
+               'буксир' => 'buksir', 'бульбул' => 'bülbül', 'бульвар' => 'bulvar', 'бульдог' => 'buldog',
+               'бульдозер' => 'buldozer', 'бульон' => 'bulyon', 'бумеранг' => 'bumerang',
                'бунен' => 'bunen', 'буннен' => 'bunnen', 'бус-бутюн' => 'büs-bütün',
-               'бутерброд' => 'buterbrod', 'буфер' => 'bufer', 'буфет' => 'bufet', 'гонъюл' => 'göñül',
-               'горизонт' => 'gorizont', 'госпиталь' => 'gospital', 'гуливер' => 'guliver', 'гуна' => 'güna',
-               'гунях' => 'günâh', 'гургуль' => 'gürgül', 'гуя' => 'güya', 'демирёл' => 'demiryol',
-               'джуньджу' => 'cüncü', 'ёлнен' => 'yolnen', 'зумбуль' => 'zümbül', 'ильи' => 'ilyi', 'ишунь' =>
-               'işün', 'кодекс' => 'kodeks', 'кодифик' => 'kodifik', 'койлю' => 'köylü', 'коккоз' =>
-               'kökköz', 'коккозь' => 'kökköz', 'коккозю' => 'kökközü', 'кокос' => 'kokos',
-               'коллег' => 'kolleg', 'коллект' => 'kollekt', 'коллекц' => 'kollekts', 'кольцов' => 'koltsov',
-               'комбин' => 'kombin', 'комедия' => 'komediya', 'коменда' => 'komenda', 'комета' => 'kometa',
-               'комис' => 'komis', 'комит' => 'komit', 'комите' => 'komite', 'коммент' => 'komment',
-               'коммерс' => 'kommers', 'коммерц' => 'kommerts', 'компенс' => 'kompens', 'компил' => 'kompil',
-               'компьютер' => 'kompyuter', 'конвейер' => 'konveyer', 'конвен' => 'konven',
-               'конверт' => 'konvert', 'конденс' => 'kondens', 'кондитер' => 'konditer',
-               'кондиц' => 'kondits', 'коник' => 'konik', 'консерв' => 'konserv', 'контейнер' => 'konteyner',
-               'континент' => 'kontinent', 'конфе' => 'konfe', 'конфискац' => 'konfiskats',
-               'концен' => 'kontsen', 'концерт' => 'kontsert', 'конъюктур' => 'konyuktur',
-               'коньки' => 'konki', 'коньяк' => 'konyak', 'копирле' => 'kopirle', 'копия' => 'kopiya',
-               'корбекул' => 'körbekül', 'кореиз' => 'koreiz', 'коренн' => 'korenn', 'корея' => 'koreya',
-               'коридор' => 'koridor', 'корнеев' => 'korneyev', 'корре' => 'korre', 'корьбекул' =>
-               'körbekül', 'косме' => 'kosme', 'космик' => 'kosmik', 'костюм' => 'kostüm', 'котельн' =>
-               'koteln', 'котировка' => 'kotirovka', 'котлет' => 'kotlet', 'кочергин' => 'koçergin',
-               'коше' => 'köşe', 'кудрин' => 'kudrin', 'кузнец' => 'kuznets', 'кулинар' => 'kulinar',
-               'кулич' => 'kuliç', 'кульминац' => 'kulminats', 'культив' => 'kultiv',
-               'культура' => 'kultura', 'куркулет' => 'kürkület', 'курсив' => 'kursiv', 'кушку' => 'küşkü',
-               'куюк' => 'küyük', 'къарагоз' => 'qaragöz', 'къолязма' => 'qolyazma', 'къуртумер' =>
-               'qurtümer', 'къуртусеин' => 'qurtüsein', 'марьино' => 'maryino', 'медьюн' => 'medyun',
-               'месули' => 'mesüli', 'месуль' => 'mesül', 'мефкуре' => 'mefküre', 'могедек' => 'mögedek',
-               'муур' => 'müür', 'муче' => 'müçe', 'муюз' => 'müyüz', 'огнево' => 'ognevo',
-               'одеколон' => 'odekolon', 'одеса' => 'odesa', 'одесса' => 'odessa', 'озерки' => 'ozerki',
-               'озерн' => 'ozern', 'озёрн' => 'ozörn', 'океан' => 'okean', 'оленев' => 'olenev',
-               'олимп' => 'olimp', 'ольчер' => 'ölçer', 'онен' => 'onen', 'оннен' => 'onnen',
-               'опера' => 'opera', 'оптим' => 'optim', 'опци' => 'optsi', 'опция' => 'optsiya',
-               'орден' => 'orden', 'ордер' => 'order', 'ореанда' => 'oreanda', 'орех' => 'oreh',
-               'оригинал' => 'original', 'ориент' => 'oriyent', 'оркестр' => 'orkestr', 'орлин' => 'orlin',
-               'офис' => 'ofis', 'офицер' => 'ofitser', 'офсет' => 'ofset', 'оюннен' => 'oyunnen', 'побед' =>
-               'pobed', 'полево' => 'polevo', 'поли' => 'poli', 'полюшко' => 'polüşko',
-               'помидор' => 'pomidor', 'пониз' => 'poniz', 'порфир' => 'porfir', 'потелов' => 'potelov',
-               'почетн' => 'poçetn', 'почётн' => 'poçötn', 'публик' => 'publik', 'публиц' => 'publits',
-               'пушкин' => 'puşkin', 'сеитумер' => 'seitümer', 'сеитусеин' => 'seitüsein', 'сеитягъя' =>
-               'seityağya', 'сеитягья' => 'seityagya', 'сеитяхья' => 'seityahya', 'сеитяя' => 'seityaya',
+               'бутерброд' => 'buterbrod', 'бутилен' => 'butilen', 'бутилир' => 'butilir',
+               'буфер' => 'bufer', 'буфет' => 'bufet', 'гобелен' => 'gobelen', 'гомео' => 'gomeo',
+               'горизонт' => 'gorizont', 'госпитал' => 'gospital', 'готтентот' => 'gottentot',
+               'гофрир' => 'gofrir', 'губерн' => 'gubern', 'гуверн' => 'guvern', 'гугенот' => 'gugenot',
+               'гуливер' => 'guliver', 'гуна' => 'güna', 'гунях' => 'günâh', 'гургуль' => 'gürgül',
+               'гуя' => 'güya', 'дёрткуль' => 'dörtkül', 'джуньджу' => 'cüncü', 'ёлнен' => 'yolnen',
+               'зумбуль' => 'zümbül', 'ильи' => 'ilyi', 'ишунь' => 'işün', 'ковер' => 'kover', 'код' => 'kod',
+               'койлю' => 'köylü', 'кокагъач' => 'kökağaç', 'кокбаштанкъара' => 'kökbaştanqara',
+               'кокгогерджин' => 'kökgögercin', 'кокдогъан' => 'kökdoğan', 'коккозю' => 'kökközü',
+               'коккъузгъун' => 'kökquzğun', 'коклюш' => 'koklüş', 'кокташ' => 'köktaş',
+               'коктогъан' => 'köktoğan', 'коктотай' => 'köktotay', 'коллег' => 'kolleg',
+               'коллект' => 'kollekt', 'коллекц' => 'kollekts', 'колье' => 'kolye', 'кольраби' => 'kolrabi',
+               'кольцов' => 'koltsov', 'комби' => 'kombi', 'комеди' => 'komedi', 'коменда' => 'komenda',
+               'комета' => 'kometa', 'комив' => 'komiv', 'комис' => 'komis', 'комит' => 'komit',
+               'комм' => 'komm', 'коммент' => 'komment', 'коммерс' => 'kommers', 'коммерц' => 'kommerts',
+               'комп' => 'komp', 'конве' => 'konve', 'конгени' => 'kongeni', 'конденс' => 'kondens',
+               'кондил' => 'kondil', 'кондитер' => 'konditer', 'кондиц' => 'kondits', 'коник' => 'konik',
+               'конкис' => 'konkis', 'консерв' => 'konserv', 'конси' => 'konsi', 'контейнер' => 'konteyner',
+               'конти' => 'konti', 'конфе' => 'konfe', 'конфи' => 'konfi', 'конце' => 'kontse',
+               'конъю' => 'konyu', 'коньки' => 'konki', 'коньяк' => 'konyak', 'копирле' => 'kopirle',
+               'копия' => 'kopiya', 'корде' => 'korde', 'кореиз' => 'koreiz', 'коренн' => 'korenn',
+               'корея' => 'koreya', 'кориа' => 'koria', 'коридор' => 'koridor', 'корне' => 'korne',
+               'корнеев' => 'korneyev', 'корни' => 'korni', 'корре' => 'korre', 'косме' => 'kosme',
+               'космик' => 'kosmik', 'костюм' => 'kostüm', 'котельн' => 'koteln', 'котир' => 'kotir',
+               'котлет' => 'kotlet', 'кочерг' => 'koçerg', 'коше' => 'köşe', 'куби' => 'kubi',
+               'кудрин' => 'kudrin', 'кузнец' => 'kuznets', 'кулинар' => 'kulinar', 'кулич' => 'kuliç',
+               'кульмин' => 'kulmin', 'культаш' => 'kültaş', 'культе' => 'külte', 'культ' => 'kult',
+               'куркулет' => 'kürkület', 'курсив' => 'kursiv', 'кушет' => 'kuşet', 'кушку' => 'küşkü',
+               'куюк' => 'küyük', 'къолязма' => 'qolyazma', 'къуртумер' => 'qurtümer',
+               'къуртусеин' => 'qurtüsein', 'медьюн' => 'medyun', 'месули' => 'mesüli',
+               'мефкуре' => 'mefküre', 'могедек' => 'mögedek', 'мумиё' => 'mumiyo', 'мумиф' => 'mumif',
+               'муче' => 'müçe', 'муюз' => 'müyüz', 'нумюне' => 'nümüne', 'обел' => 'obel', 'обер' => 'ober',
+               'обли' => 'obli', 'обсе' => 'obse', 'обт' => 'obt', 'огне' => 'ogne', 'одеколон' => 'odekolon',
+               'одеса' => 'odesa', 'одесса' => 'odessa', 'озерки' => 'ozerki', 'озерн' => 'ozern',
+               'озёрн' => 'ozörn', 'озюя' => 'özüya', 'океан' => 'okean', 'окси' => 'oksi',
+               'октет' => 'oktet', 'олеа' => 'olea', 'олеи' => 'olei', 'оленев' => 'olenev', 'олив' => 'oliv',
+               'олиг' => 'olig', 'олимп' => 'olimp', 'олиф' => 'olif', 'ольчер' => 'ölçer', 'омле' => 'omle',
+               'онен' => 'onen', 'оннен' => 'onnen', 'опера' => 'opera', 'опере' => 'opere',
+               'оптим' => 'optim', 'опци' => 'optsi', 'орби' => 'orbi', 'орден' => 'orden',
+               'ордер' => 'order', 'ордин' => 'ordin', 'ореа' => 'orea', 'орех' => 'oreh',
+               'ориент' => 'oriyent', 'оркестр' => 'orkestr', 'орлин' => 'orlin', 'орни' => 'orni',
+               'орхи' => 'orhi', 'осци' => 'ostsi', 'офис' => 'ofis', 'офиц' => 'ofits', 'офсет' => 'ofset',
+               'очерк' => 'oçerk', 'оюннен' => 'oyunnen', 'побед' => 'pobed', 'полево' => 'polevo',
+               'поли' => 'poli', 'полюшко' => 'polüşko', 'помидор' => 'pomidor', 'пониз' => 'poniz',
+               'порфир' => 'porfir', 'потелов' => 'potelov', 'потюк' => 'pötük', 'почетн' => 'poçetn',
+               'почётн' => 'poçötn', 'пукле' => 'pükle', 'пуркю' => 'pürkü', 'пурумют' => 'purümüt',
+               'пускул' => 'püskül', 'пускур' => 'püskür', 'пусюр' => 'püsür', 'пуфле' => 'püfle',
                'сейитумер' => 'seyitümer', 'сейитусеин' => 'seyitüsein', 'сейитягъя' => 'seyityağya',
                'сейитягья' => 'seyityagya', 'сейитяхья' => 'seyityahya', 'сейитяя' => 'seyityaya',
-               'ультимат' => 'ultimat', 'ультра' => 'ultra', 'ульянов' => 'ulyanov', 'универ' => 'univer',
-               'уника' => 'unika', 'унтер' => 'unter', 'урьян' => 'uryan', 'уткин' => 'utkin', 'учебн' =>
-               'uçebn', 'шовини' => 'şovini', 'шоссе' => 'şosse', 'шубин' => 'şubin', 'шунен' => 'şunen',
-               'шуннен' => 'şunnen', 'щёлкино' => 'şçolkino', 'эмирусеин' => 'emirüsein',
-               'юзбашы' => 'yüzbaşı', 'юзйыл' => 'yüzyıl', 'юртер' => 'yurter', 'ющенко' => 'yuşçenko',
-
-               'кою' => 'köyü', 'кок' => 'kök', 'ком-кок' => 'köm-kök', 'коп' => 'köp', 'ог' => 'ög',
-               'юрип' => 'yürip', 'юз' => 'yüz', 'юк' => 'yük', 'буюп' => 'büyüp', 'буюк' => 'büyük',
-               'джонк' => 'cönk', 'джонкю' => 'cönkü', 'устке' => 'üstke', 'устте' => 'üstte',
-               'усттен' => 'üstten',
-
-               # шофёр needs to come after шофер to override it in the Latin-to-Cyrillic direction
-               'шофер' => 'şoför',
-               'шофёр' => 'şoför',
-
-               #### originally Latin to Cyrillic (deduped from above)
+               'сеитумер' => 'seitümer', 'сеитусеин' => 'seitüsein', 'сеитягъя' => 'seityağya',
+               'сеитягья' => 'seityagya', 'сеитяхья' => 'seityahya', 'сеитяя' => 'seityaya',
+               'сурет' => 'süret', 'увертюра' => 'uvertüra', 'угле' => 'ugle', 'узвий' => 'uzviy',
+               'улица' => 'ulitsa', 'ультимат' => 'ultimat', 'ультра' => 'ultra', 'ульянов' => 'ulyanov',
+               'универ' => 'univer', 'уник' => 'unik', 'унис' => 'unis', 'унит' => 'unit', 'униф' => 'unif',
+               'унтер' => 'unter', 'урьян' => 'uryan', 'утил' => 'util', 'уткин' => 'utkin',
+               'учебн' => 'uçebn', 'шовини' => 'şovini', 'шоссе' => 'şosse', 'шубин' => 'şubin',
+               'шунен' => 'şunen', 'шуннен' => 'şunnen', 'шунчюн' => 'şunçün', 'щёлкино' => 'şçolkino',
+               'эмирусеин' => 'emirüsein', 'юзбашы' => 'yüzbaşı', 'юзйыл' => 'yüzyıl', 'юртер' => 'yurter',
+               'ющенко' => 'yuşçenko',
+
+               ### Carefully ordered many-to-one mappings
+               # these are ordered so L2C is correct (the later Cyrillic one)
+               # see also $ManyToOneC2LMappings above for C2L
+               'шофер' => 'şoför', 'шофёр' => 'şoför',
+               'бугун' => 'bugün', 'бугунь' => 'bugün',
+               'демирёл' => 'demiryol', 'демиръёл' => 'demiryol',
+               'гонъюл' => 'göñül', 'гонъюль' => 'göñül',
+               'коккоз' => 'kökköz', 'коккозь' => 'kökköz',
+               'корбекул' => 'körbekül', 'корьбекул' => 'körbekül', 'корьбекуль' => 'körbekül',
+               'муур' => 'müür', 'муурь' => 'müür',
+               'оригинал' => 'original', 'оригиналь' => 'original',
+               'пускю' => 'püskü', 'пуськю' => 'püskü',
+               'къарагоз' => 'qaragöz', 'къарагозь' => 'qaragöz',
+               'етсин' => 'yetsin', 'етсин' => 'etsin',
+
+               #### Latin to Cyrillic (deduped from above)
 
                # слова на -аль
                # words in -аль
@@ -184,42 +224,39 @@ class CrhExceptions {
                'истикъбаль' => 'istiqbal', 'истикъляль' => 'istiqlâl', 'италия' => 'italiya',
                'италья' => 'italya', 'ишгъаль' => 'işğal', 'кафедраль' => 'kafedral', 'казуаль' => 'kazual',
                'коллегиаль' => 'kollegial', 'колоссаль' => 'kolossal', 'коммуналь' => 'kommunal',
-               'кординаль' => 'kordinal', 'криминаль' => 'kriminal', 'легаль' => 'legal', 'леталь' => 'letal',
-               'либеÑ\80алÑ\8c' => 'liberal', 'локалÑ\8c' => 'lokal', 'магиÑ\81Ñ\82Ñ\80алÑ\8c' => 'magistral',
-               'материаль' => 'material', 'машиналь' => 'maşinal', 'меаль' => 'meal',
-               'медалÑ\8cон' => 'medalyon', 'медалÑ\8c' => 'medal', 'меÑ\80идионалÑ\8c' => 'meridional',
-               'меÑ\88Ñ\8aалÑ\8c' => 'meÅ\9fal', 'минеÑ\80алÑ\8c' => 'mineral', 'минималÑ\8c' => 'minimal', 'миÑ\81алÑ\8c' => 'misal',
-               'модалÑ\8c' => 'modal', 'мÑ\83зÑ\8bкалÑ\8c' => 'muzıkal', 'номиналÑ\8c' => 'nominal', 'ноÑ\80малÑ\8c' => 'normal',
-               'опÑ\82ималÑ\8c' => 'optimal', 'оÑ\80биÑ\82алÑ\8c' => 'orbital', 'оÑ\80игиналÑ\8c' => 'original',
-               'педалÑ\8c' => 'pedal', 'пÑ\80опоÑ\80Ñ\86ионалÑ\8c' => 'proportsional', 'пÑ\80оÑ\84еÑ\81Ñ\81ионалÑ\8c' => 'professional',
-               'радикаль' => 'radikal', 'рациональ' => 'ratsional', 'реаль' => 'real',
-               'региональ' => 'regional', 'суаль' => 'sual', 'шималь' => 'şimal',
+               'кординаль' => 'kordinal', 'криминаль' => 'kriminal', 'легаль' => 'legal',
+               'леÑ\82алÑ\8c' => 'letal', 'либеÑ\80алÑ\8c' => 'liberal', 'локалÑ\8c' => 'lokal',
+               'магистраль' => 'magistral', 'материаль' => 'material', 'машиналь' => 'maşinal',
+               'меалÑ\8c' => 'meal', 'медалÑ\8cон' => 'medalyon', 'медалÑ\8c' => 'medal',
+               'меÑ\80идионалÑ\8c' => 'meridional', 'меÑ\88Ñ\8aалÑ\8c' => 'meÅ\9fal', 'минеÑ\80алÑ\8c' => 'mineral',
+               'минималÑ\8c' => 'minimal', 'миÑ\81алÑ\8c' => 'misal', 'модалÑ\8c' => 'modal', 'мÑ\83зÑ\8bкалÑ\8c' => 'muzıkal',
+               'номиналÑ\8c' => 'nominal', 'ноÑ\80малÑ\8c' => 'normal', 'опÑ\82ималÑ\8c' => 'optimal',
+               'оÑ\80биÑ\82алÑ\8c' => 'orbital', 'педалÑ\8c' => 'pedal', 'пÑ\80опоÑ\80Ñ\86ионалÑ\8c' => 'proportsional',
+               'профессиональ' => 'professional', 'радикаль' => 'radikal', 'рациональ' => 'ratsional',
+               'Ñ\80еалÑ\8c' => 'real', 'Ñ\80егионалÑ\8c' => 'regional', 'Ñ\81Ñ\83алÑ\8c' => 'sual', 'Ñ\88ималÑ\8c' => 'Å\9fimal',
                'территориаль' => 'territorial', 'тимсаль' => 'timsal', 'тоталь' => 'total',
                'уникаль' => 'unikal', 'универсаль' => 'universal', 'вертикаль' => 'vertikal',
                'виртуаль' => 'virtual', 'визуаль' => 'vizual', 'вуаль' => 'vual', 'зональ' => 'zonal',
-               'зуаль' => 'zual',
+               'зуаль' => 'zual', 'италь' => 'ital',
 
                # слова с мягким знаком перед а, о, у, э
                # Words with a soft sign before а, о, у, э
-               'бильакис' => 'bilakis', 'маальэсеф' => 'maalesef',
-               'мельун' => 'melun', 'озьара' => 'özara', 'вельасыл' => 'velasıl',
-               'ельаякъ' => 'yelayaq',
-               # these are ordered so C2L is correct (the later Latin one)
-               'февкъульаде' => 'fevqülade','февкъульаде' => 'fevqulade',
+               'бильакис' => 'bilakis', 'маальэсеф' => 'maalesef', 'мельун' => 'melun', 'озьара' => 'özara',
+               'вельасыл' => 'velasıl', 'ельаякъ' => 'yelayaq',
 
                # другие слова с мягким знаком
                # Other words with a soft sign
                'альбатрос' => 'albatros', 'альбинос' => 'albinos', 'альбом' => 'albom',
                'альбумин' => 'albumin', 'алфавит' => 'alfavit', 'альфа' => 'alfa', 'альманах' => 'almanah',
-               'альпинист' => 'alpinist', 'альтерн' => 'altern', 'альтру' => 'altru', 'альвеола' => 'alveola',
-               'анÑ\81амблÑ\8c' => 'ansambl', 'анÑ\8cане' => 'anane', 'аÑ\81Ñ\84алÑ\8cÑ\82' => 'asfalt', 'балÑ\8cнео' => 'balneo',
-               'бааÑ\80Ñ\8c' => 'baar', 'базалÑ\8cÑ\82' => 'bazalt', 'биноклÑ\8c' => 'binokl', 'джÑ\83Ñ\80Ñ\8cаÑ\82' => 'curat',
-               'джÑ\83Ñ\80Ñ\8cаÑ\82' => 'cürat', 'девалÑ\8cв' => 'devalv', 'Ñ\84акÑ\83лÑ\8cÑ\82' => 'fakult', 'Ñ\84алÑ\8cÑ\81иÑ\84' => 'falsif',
-               'фольклор' => 'folklor', 'гальван' => 'galvan', 'геральд' => 'gerald', 'женьшень' => 'jenşen',
+               'альпинист' => 'alpinist', 'альтерн' => 'altern', 'альтру' => 'altru',
+               'алÑ\8cвеола' => 'alveola', 'анÑ\81амблÑ\8c' => 'ansambl', 'анÑ\8cане' => 'anane', 'аÑ\81Ñ\84алÑ\8cÑ\82' => 'asfalt',
+               'балÑ\8cнео' => 'balneo', 'бааÑ\80Ñ\8c' => 'baar', 'базалÑ\8cÑ\82' => 'bazalt', 'биноклÑ\8c' => 'binokl',
+               'девалÑ\8cв' => 'devalv', 'Ñ\84акÑ\83лÑ\8cÑ\82' => 'fakult', 'Ñ\84алÑ\8cÑ\81иÑ\84' => 'falsif', 'Ñ\84олÑ\8cклоÑ\80' => 'folklor',
+               'гальван' => 'galvan', 'геральд' => 'gerald', 'женьшень' => 'jenşen',
                'инвентарь' => 'inventar', 'кальк' => 'kalk', 'кальмар' => 'kalmar', 'консульт' => 'konsult',
-               'контроль' => 'kontrol', 'кульмин' => 'kulmin', 'культур' => 'kultur', 'лагерь' => 'lager',
-               'макъбуль' => 'maqbul', 'макъуль' => 'maqul', 'мальт' => 'malt', 'мальземе' => 'malzeme',
-               'меджуль' => 'mecul', 'мешгуль' => 'meşgül', 'мешгъуль' => 'meşğul', 'мульти' => 'multi',
+               'контроль' => 'kontrol', 'культур' => 'kultur', 'лагерь' => 'lager', 'макъбуль' => 'maqbul',
+               'макъуль' => 'maqul', 'мальт' => 'malt', 'мальземе' => 'malzeme', 'меджуль' => 'mecul',
+               'мешгуль' => 'meşgül', 'мешгъуль' => 'meşğul', 'мульти' => 'multi',
                'мусульман' => 'musulman', 'нефть' => 'neft', 'пальто' => 'palto', 'пароль' => 'parol',
                'патруль' => 'patrul', 'пенальти' => 'penalti', 'къальби' => 'qalbi', 'къальпке' => 'qalpke',
                'къальплер' => 'qalpler', 'къальпни' => 'qalpni', 'къальпте' => 'qalpte', 'къаарь' => 'qaar',
@@ -233,23 +270,23 @@ class CrhExceptions {
                # слова с твёрдым знаком
                # Words with a solid sign
                'бидъат' => 'bidat', 'бузъюрек' => 'buzyürek', 'атешъюрек' => 'ateşyürek',
-               'алÑ\8aÑ\8fнакÑ\8a' => 'alyanaq', 'демиÑ\80Ñ\8aÑ\91л' => 'demiryol', 'деÑ\80Ñ\8aал' => 'deral', 'инÑ\8aекÑ\86' => 'inyekts',
-               'меÑ\84Ñ\8aÑ\83м' => 'mefum', 'меÑ\88Ñ\8aÑ\83м' => 'meÅ\9fum', 'обÑ\8aекÑ\82' => 'obyekt', 'Ñ\80азÑ\8aезд' => 'razyezd',
-               'Ñ\81Ñ\83бÑ\8aекÑ\82' => 'subyekt', 'Ñ\85авÑ\8aÑ\8fÑ\80' => 'havyar', 'Ñ\8fмÑ\8aÑ\8fм' => 'yamyam',
+               'алÑ\8aÑ\8fнакÑ\8a' => 'alyanaq', 'инÑ\8aекÑ\86' => 'inyekts', 'меÑ\84Ñ\8aÑ\83м' => 'mefum', 'меÑ\88Ñ\8aÑ\83м' => 'meÅ\9fum',
+               'обÑ\8aекÑ\82' => 'obyekt', 'Ñ\80азÑ\8aезд' => 'razyezd', 'Ñ\81Ñ\83бÑ\8aекÑ\82' => 'subyekt', 'Ñ\85авÑ\8aÑ\8fÑ\80' => 'havyar',
+               'ямъям' => 'yamyam',
 
                # слова с буквой щ
                # words with щ
                'ящик' => 'yaşçik', 'мещан' => 'meşçan',
 
-               # слова с буквой ц
+               # слова с ц
                # words with ц
                'акциз' => 'aktsiz', 'ацет' => 'atset', 'блиц' => 'blits', 'бруцеллёз' => 'brutsellöz',
                'доцент' => 'dotsent', 'фармацевт' => 'farmatsevt', 'глицер' => 'glitser',
                'люцерна' => 'lütserna', 'лицей' => 'litsey', 'меццо' => 'metstso', 'наци' => 'natsi',
                'проце' => 'protse', 'рецеп' => 'retsep', 'реценз' => 'retsenz', 'теплица' => 'teplitsa',
-               'виÑ\86е' => 'vitse', 'Ñ\86епÑ\81' => 'tseps', 'Ñ\88вейÑ\86аÑ\80' => 'Å\9fveytsar',
+               'вице' => 'vitse', 'швейцар' => 'şveytsar',
 
-               # слова без буквы тс
+               # слова с тс
                # words with тс
                'агъартс' => 'ağarts', 'агъыртс' => 'ağırts', 'бильдиртс' => 'bildirts', 'битсин' => 'bitsin',
                'буюльтс' => 'büyülts', 'буютс' => 'büyüts', 'гебертс' => 'geberts', 'делиртс' => 'delirts',
@@ -259,253 +296,55 @@ class CrhExceptions {
                'кучертс' => 'küçerts', 'кучюльтс' => 'küçülts', 'пертсин' => 'pertsin', 'къайтс' => 'qayts',
                'къутсуз' => 'qutsuz', 'орьтс' => 'örts', 'отьс' => 'öts', 'тартс' => 'tarts',
                'тутсун' => 'tutsun', 'тюнъюльтс' => 'tüñülts', 'тюртс' => 'türts', 'янъартс' => 'yañarts',
-               'ебеÑ\80Ñ\82Ñ\81' => 'yeberts', 'еÑ\82Ñ\81ин' => 'yetsin', 'еÑ\88еÑ\80Ñ\82Ñ\81' => 'yeÅ\9ferts', 'йиÑ\80иÑ\82Ñ\81' => 'yirits',
+               'ебертс' => 'yeberts', 'ешертс' => 'yeşerts', 'йиритс' => 'yirits',
 
                # разные исключения
                # different exceptions
-               'бейуде' => 'beyude', 'бугунь' => 'bugün', 'бюджет' => 'bücet', 'бюллет' => 'büllet',
-               'бюро' => 'büro', 'бюст' => 'büst', 'джонк' => 'cönk', 'диалог' => 'dialog',
-               'гонъюль' => 'göñül', 'ханымэфенди' => 'hanımefendi', 'каньон' => 'kanyon', 'кирил' => 'kiril',
-               'кирил' => 'kirill', 'кёрджа' => 'körca', 'кой' => 'köy', 'кулеръюзь' => 'küleryüz',
-               'маалле' => 'маальle', 'майор' => 'mayor', 'маниал' => 'manиаль', 'мефкуре' => 'mefküre',
-               'месуль' => 'mesul', 'месуль' => 'mesül', 'муурь' => 'müür',
-               'нормала' => 'нормальa', 'нумюне' => 'nümüne', 'проект' => 'proekt', 'район' => 'rayon',
-               'сойады' => 'soyadı', 'спортсмен' => 'sportsmen', 'услюп' => 'üslüp', 'услюб' => 'üslüb',
-               'вакъиал' => 'vaqиаль', 'юзйыллыкъ' => 'yüzyıllıq',
+               'бюджет' => 'bücet', 'бюллет' => 'büllet', 'бюро' => 'büro', 'бюст' => 'büst',
+               'диалог' => 'dialog', 'ханымэфенди' => 'hanımefendi', 'каньон' => 'kanyon',
+               'кирил' => 'kiril', 'кирилл' => 'kirill', 'кёрджа' => 'körca', 'коy' => 'köy',
+               'кулеръюзь' => 'küleryüz', 'маалле' => 'маальle', 'майор' => 'mayor', 'маниал' => 'manиаль',
+               'нормала' => 'нормальa', 'проект' => 'proekt', 'район' => 'rayon', 'сойады' => 'soyadı',
+               'спортсмен' => 'sportsmen', 'услюп' => 'üslüp', 'услюб' => 'üslüb', 'вакъиал' => 'vaqиаль',
+               'юзйыллыкъ' => 'yüzyıllıq', 'койот' => 'koyot',
 
                # имена собственные
                # proper names
-               'адольф' => 'adolf', 'альберт' => 'albert', 'бешуй' => 'beşüy', 'эмирусеин' => 'emirüsein',
-               'флотск' => 'flotsk', 'гайана' => 'gayana', 'грэсовский' => 'gresovskiy', 'гриц' => 'grits',
-               'гурджи' => 'gürci', 'игорь' => 'igor', 'ильич' => 'ilyiç', 'ильин' => 'ilyin',
-               'исмаил' => 'ismail', 'киттс' => 'kitts', 'комсомольск' => 'komsomolsk',
-               'корьбекулю' => 'körbekülü', 'корьбекуль' => 'körbekül', 'куницын' => 'kunitsın',
-               'львив' => 'lviv', 'львов' => 'lvov', 'марьино' => 'maryino', 'махульдюр' => 'mahuldür',
-               'павел' => 'pavel', 'пантикапейон' => 'pantikapeyon', 'къарагозь' => 'qaragöz',
-               'къуртсейит' => 'qurtseyit', 'къуртсеит' => 'qurtseit', 'къуртумер' => 'qurtümer',
-               'сейитумер' => 'seyitümer', 'сеитумер' => 'seitümer', 'смаил' => 'smail',
-               'советск' => 'sovetsk', 'шемьи-заде' => 'şemi-zade', 'щёлкино' => 'şçolkino',
-               'тсвана' => 'tsvana', 'учьэвли' => 'üçevli', 'йохан' => 'yohan', 'йорк' => 'york',
-               'ющенко' => 'yuşçenko', 'льная' => 'lnaya', 'льное' => 'lnoye', 'льный' => 'lnıy',
-               'льская' => 'lskaya', 'льский' => 'lskiy', 'льское' => 'lskoye', 'ополь' => 'opol',
+               'адольф' => 'adolf', 'альберт' => 'albert', 'бешуй' => 'beşüy', 'флотск' => 'flotsk',
+               'гайана' => 'gayana', 'грэсовский' => 'gresovskiy', 'гриц' => 'grits', 'гурджи' => 'gürci',
+               'игорь' => 'igor', 'ильич' => 'ilyiç', 'ильин' => 'ilyin', 'исмаил' => 'ismail',
+               'киттс' => 'kitts', 'комсомольск' => 'komsomolsk', 'корьбекулю' => 'körbekülü',
+               'куницын' => 'kunitsın', 'львив' => 'lviv', 'львов' => 'lvov', 'марьино' => 'maryino',
+               'махульдюр' => 'mahuldür', 'павел' => 'pavel', 'пантикапейон' => 'pantikapeyon',
+               'къуртсейит' => 'qurtseyit', 'къуртсеит' => 'qurtseit', 'смаил' => 'smail',
+               'советск' => 'sovetsk', 'шемьи-заде' => 'şemi-zade', 'тсвана' => 'tsvana',
+               'учьэвли' => 'üçevli', 'йохан' => 'yohan', 'йорк' => 'york', 'винныця' => 'vinnıtsâ',
+               'винница' => 'vinnitsa', 'хмельницк' => 'hmelnitsk', 'хмельныцк' => 'hmelnıtsk',
+               'зайце' => 'zaytse', 'чистеньк' => 'çistenk', 'кольчуг' => 'kolçug', 'ручьи' => 'ruçyi',
+               'ботсвана' => 'botsvana', 'большой' => 'bolşoy', 'большое' => 'bolşoye',
+               'большая' => 'bolşaya', 'ущелье' => 'uşçelye', 'ущельное' => 'uşçelnoye',
+               'предущельное' => 'preduşçelnoye', 'новенькое' => 'novenkoye', 'новосельц' => 'novoselts',
+               'мелко' => 'melko', 'овощ' => 'ovoşç', 'перепёлк' => 'perepölk', 'рощин' => 'roşçin',
+               'братск' => 'bratsk', 'краснофлотск' => 'krasnoflotsk', 'синицин' => 'sinitsin',
+               'синицын' => 'sinitsın', 'льгов' => 'lgov', 'желто' => 'jelto', 'жёлт' => 'jölt',
+               'пермь' => 'perm', 'солдатск' => 'soldatsk', 'кольцо' => 'koltso', 'шелко' => 'şelko',
+               'охотск' => 'ohotsk', 'марий эл' => 'mariy el', 'мариуполь' => 'mariupol',
+               'белгород' => 'belgorod', 'иркутск' => 'irkutsk', 'Иркутск' => 'İrkutsk', 'орёл' => 'oröl',
+               'рязанск' => 'râzansk', 'рязань' => 'râzan', 'тверск' => 'tversk', 'тверь' => 'tver',
+               'ярославль' => 'yaroslavl', 'благовеще' => 'blagoveşçe', 'мальдив' => 'maldiv',
+               'бальбек' => 'balbek', 'альчик' => 'alçik', 'харьков' => 'harkov', 'волынск' => 'volınsk',
+               'волынь' => 'volın',
 
-               # originally Latin to Cyrillic, deduped from above
-               'ань' => 'an', 'аньге' => 'ange', 'аньде' => 'ande', 'аньки' => 'anki', 'кёр' => 'kör',
-               'мэр' => 'mer', 'этсин' => 'etsin',
-
-               # exceptions added after speaker review
-               # see https://www.mediawiki.org/wiki/User:TJones_(WMF)/T23582
-               'аджизленювинъиз' => 'acizlenüviñiz', 'акъшам' => 'aqşam', 'алчакъгонъюлли' => 'alçaqgöñülli',
-               'аньанелер' => 'ananeler', 'аньанелеримиз' => 'ananelerimiz',
-               'аньанелеримизден' => 'ananelerimizden', 'аньанелеримизни' => 'ananelerimizni',
-               'аньанели' => 'ananeli', 'асфальтке' => 'asfaltke', 'баарьде' => 'baarde', 'бахтсыз' => 'bahtsız',
-               'берилюви' => 'berilüvi', 'берювден' => 'berüvden', 'берювни' => 'berüvni',
-               'большевиклер' => 'bolşevikler', 'большевиклерге' => 'bolşeviklerge', 'болюк' => 'bölük',
-               'болюнген' => 'bölüngen', 'болюнгенини' => 'bölüngenini', 'болюшип' => 'bölüşip',
-               'бугуннинъ' => 'bugünniñ', 'бугуньден' => 'bugünden', 'бугуньки' => 'bugünki',
-               'букюльген' => 'bükülgen', 'букюльди' => 'büküldi', 'буллюр' => 'büllür',
-               'бурюмчик' => 'bürümçik', 'бурюнген' => 'bürüngen', 'бутюн' => 'bütün', 'бутюнлей' => 'bütünley',
-               'буюген' => 'büyügen', 'буюй' => 'büyüy', 'волость' => 'volost', 'волостьларгъа' => 'volostlarğa',
-               'гонъюлини' => 'göñülini', 'гонъюлли' => 'göñülli', 'гонъюллилер' => 'göñülliler',
-               'госпиталинде' => 'gospitalinde', 'госпитальге' => 'gospitalge', 'госпитальде' => 'gospitalde',
-               'гренадёр' => 'grenadör', 'гугюм' => 'gügüm', 'гугюмлер' => 'gügümler',
-               'гугюмлери' => 'gügümleri', 'гугюмлерини' => 'gügümlerini', 'гурьсюльди' => 'gürsüldi',
-               'гурюльдештилер' => 'gürüldeştiler', 'гурюльти' => 'gürülti', 'гурюльтили' => 'gürültili',
-               'гурюльтисидир' => 'gürültisidir', 'дарульмуаллиминде' => 'darülmualliminde',
-               'дарульмуаллимининде' => 'darülmuallimininde', 'дарульмуаллиминнинъ' => 'darülmualliminniñ',
-               'дёгюльген' => 'dögülgen', 'декабрьде' => 'dekabrde', 'дёндюрилип' => 'döndürilip',
-               'дёнермиз' => 'dönermiz', 'дёнмектелер' => 'dönmekteler', 'денъишюв' => 'deñişüv',
-               'дёрдю' => 'dördü', 'дёрдюмиз' => 'dördümiz', 'дёрдюнджи' => 'dördünci', 'дёрт' => 'dört',
-               'дертлешювге' => 'dertleşüvge', 'джесюр' => 'cesür', 'джесюране' => 'cesürane',
-               'джесюрликлерини' => 'cesürliklerini', 'джонегенлерини' => 'cönegenlerini',
-               'джонедим' => 'cönedim', 'джонейлер' => 'cöneyler', 'джурьатсызлыгъына' => 'cüratsızlığına',
-               'дюгюнлер' => 'dügünler', 'дюгюнлерле' => 'dügünlerle', 'дюдюк' => 'düdük', 'дюльбер' => 'dülber',
-               'дюльбери' => 'dülberi', 'дюльберлер' => 'dülberler', 'дюльберлернинъ' => 'dülberlerniñ',
-               'дюльгер' => 'dülger', 'дюльгерге' => 'dülgerge', 'дюльгерлернинъки' => 'dülgerlerniñki',
-               'дюльгерни' => 'dülgerni', 'дюльгернинъ' => 'dülgerniñ', 'дюмбюрдетти' => 'dümbürdetti',
-               'дюмен' => 'dümen', 'дюмени' => 'dümeni', 'дюнья' => 'dünya', 'дюньявий' => 'dünyaviy',
-               'дюньяда' => 'dünyada', 'дюньяларгъа' => 'dünyalarğa', 'дюньяларда' => 'dünyalarda',
-               'дюньяны' => 'dünyanı', 'дюньянынъ' => 'dünyanıñ', 'дюньясы' => 'dünyası',
-               'ельаякълылар' => 'yelayaqlılar', 'елькъуваны' => 'yelquvanı', 'ильич' => 'i̇liç',
-               'ичюн' => 'içün', 'ичюнми' => 'içünmi', 'келюви' => 'kelüvi', 'келювини' => 'kelüvini',
-               'келювинъизде' => 'kelüviñizde', 'келювни' => 'kelüvni', 'кемирювлер' => 'kemirüvler',
-               'кесювде' => 'kesüvde', 'кетюв' => 'ketüv', 'кетювге' => 'ketüvge', 'кетюви' => 'ketüvi',
-               'кетювимни' => 'ketüvimni', 'кетювлер' => 'ketüvler', 'кетювлери' => 'ketüvleri',
-               'кетювлеринънинъ' => 'ketüvleriñniñ', 'кетювнинъ' => 'ketüvniñ', 'кирюв' => 'kirüv',
-               'князь' => 'knâz', 'козькъапакъларыны' => 'közqapaqlarını', 'козьлю' => 'közlü', 'козю' => 'közü',
-               'козюме' => 'közüme', 'козюнде' => 'közünde', 'козюне' => 'közüne', 'козюнен' => 'közünen',
-               'козюнинъ' => 'közüniñ', 'козюнъни' => 'közüñni', 'койлюде' => 'köylüde',
-               'койлюлер' => 'köylüler', 'койлюлерде' => 'köylülerde', 'койлюлерни' => 'köylülerni',
-               'койлюлернинъ' => 'köylülerniñ', 'койлюнинъ' => 'köylüniñ', 'коккозьге' => 'kökközge',
-               'коккозьде' => 'kökközde', 'коккозьдеки' => 'kökközdeki', 'коккозьден' => 'kökközden',
-               'кокюс' => 'köküs', 'кокюси' => 'köküsi', 'кокюсим' => 'köküsim', 'кокюсиме' => 'köküsime',
-               'кокюсинъе' => 'köküsiñe', 'комиссарлар' => 'komissarlar', 'комиссарлары' => 'komissarları',
-               'комитетининъ' => 'komitetiniñ', 'концлагерь' => 'kontslager', 'копьмеди' => 'köpmedi',
-               'копьти' => 'köpti', 'копюр' => 'köpür', 'копюрге' => 'köpürge', 'копюрден' => 'köpürden',
-               'копюри' => 'köpüri', 'копюрнинъ' => 'köpürniñ', 'коридорда' => 'koridorda',
-               'корьсюн' => 'körsün', 'корюв' => 'körüv', 'корюльген' => 'körülgen', 'корюнди' => 'köründi',
-               'корюндинъ' => 'köründiñ', 'корюне' => 'körüne', 'корюнип' => 'körünip',
-               'корюнмеген' => 'körünmegen', 'корюнмеди' => 'körünmedi', 'корюнмедилер' => 'körünmediler',
-               'корюнмей' => 'körünmey', 'корюнмейсинъиз' => 'körünmeysiñiz', 'корюнмекте' => 'körünmekte',
-               'корюнмектелер' => 'körünmekteler', 'корюнъиз' => 'körüñiz', 'корюше' => 'körüşe',
-               'корюшеджекмиз' => 'körüşecekmiz', 'корюшим' => 'körüşim', 'корюшип' => 'körüşip',
-               'корюширмиз' => 'körüşirmiz', 'корюшкен' => 'körüşken', 'корюшкенде' => 'körüşkende',
-               'корюшмеге' => 'körüşmege', 'корюшмегенимиз' => 'körüşmegenimiz', 'корюштик' => 'körüştik',
-               'корюштим' => 'körüştim', 'корюшюв' => 'körüşüv', 'корюшювде' => 'körüşüvde',
-               'корюшювден' => 'körüşüvden', 'корюшюви' => 'körüşüvi', 'корюшювимден' => 'körüşüvimden',
-               'корюшювимизге' => 'körüşüvimizge', 'корюшювимизден' => 'körüşüvimizden',
-               'костюми' => 'kostümi', 'кузю' => 'küzü', 'кулькюден' => 'külküden', 'кулькюнинъ' => 'külküniñ',
-               'кулькюсининъ' => 'külküsiniñ', 'кулю' => 'külü', 'кулюмсиреген' => 'külümsiregen',
-               'кулюмсиреди' => 'külümsiredi', 'кулюмсиредим' => 'külümsiredim', 'кулюмсирей' => 'külümsirey',
-               'кулюмсирейим' => 'külümsireyim', 'кулюмсиреп' => 'külümsirep', 'кулюни' => 'külüni',
-               'кулюнчли' => 'külünçli', 'кулюшинде' => 'külüşinde', 'кулюштилер' => 'külüştiler',
-               'кумюш' => 'kümüş', 'куньдюз' => 'kündüz', 'куньдюзлери' => 'kündüzleri', 'куньлюк' => 'künlük',
-               'куню' => 'künü', 'кунюмде' => 'künümde', 'кунюнде' => 'kününde', 'кунюндеми' => 'künündemi',
-               'кунюнъ' => 'künüñ', 'курькчю' => 'kürkçü', 'курьсю' => 'kürsü', 'курьсюге' => 'kürsüge',
-               'курьсюлер' => 'kürsüler', 'курючтен' => 'kürüçten', 'кутюклерни' => 'kütüklerni',
-               'кутюкли' => 'kütükli', 'кучьлю' => 'küçlü', 'кучьлюклер' => 'küçlükler',
-               'кучьсюнмезсинъ' => 'küçsünmezsiñ', 'кучюджик' => 'küçücik', 'кучюк' => 'küçük',
-               'кучюм' => 'küçüm', 'кучюмле' => 'küçümle', 'кучюнден' => 'küçünden', 'кучюни' => 'küçüni',
-               'къаарьлене' => 'qaarlene', 'къаарьли' => 'qaarli', 'къальбим' => 'qalbim',
-               'къальбимни' => 'qalbimni', 'къальбинде' => 'qalbinde', 'къальпли' => 'qalpli',
-               'къальптен' => 'qalpten', 'къалюбелядан' => 'qalübelâdan', 'къулюбенъде' => 'qulübeñde',
-               'лёман' => 'löman', 'львованынъ' => 'lvovanıñ', 'лютфи' => 'lütfi', 'лютфиге' => 'lütfige',
-               'лютфини' => 'lütfini', 'мазюн' => 'mazün', 'малюм' => 'malüm', 'малюмат' => 'malümat',
-               'махлюкъаттан' => 'mahlüqattan', 'махлюкътан' => 'mahlüqtan', 'махульдюрге' => 'mahuldürge',
-               'махульдюрде' => 'mahuldürde', 'махульдюрдеки' => 'mahuldürdeki',
-               'махульдюрден' => 'mahuldürden', 'махульдюрли' => 'mahuldürli',
-               'махульдюрлилер' => 'mahuldürliler', 'махульдюрлилермиз' => 'mahuldürlilermiz',
-               'махульдюрми' => 'mahuldürmi', 'махульдюрни' => 'mahuldürni', 'мевджут' => 'mevcut',
-               'мезкюр' => 'mezkür', 'мектюп' => 'mektüp', 'мектюпни' => 'mektüpni', 'мектюпте' => 'mektüpte',
-               'мелитопольге' => 'melitopolge', 'мемнюн' => 'memnün', 'мемнюниетле' => 'memnüniyetle',
-               'мемнюним' => 'memnünim', 'мемнюнмиз' => 'memnünmiz', 'менсюп' => 'mensüp',
-               'мешгъульмиз' => 'meşğulmiz', 'мулькюни' => 'mülküni', 'мумкюн' => 'mümkün',
-               'мумкюнми' => 'mümkünmi', 'мусульманлар' => 'musulmanlar', 'мусульманлармы' => 'musulmanlarmı',
-               'мухкемлендирюв' => 'mühkemlendirüv', 'мушкюль' => 'müşkül', 'ничюн' => 'niçün',
-               'ничюндир' => 'niçündir', 'нумюнеси' => 'nümünesi', 'огю' => 'ögü', 'огюз' => 'ögüz',
-               'огюмде' => 'ögümde', 'огюмдеки' => 'ögümdeki', 'огюме' => 'ögüme', 'огюмизге' => 'ögümizge',
-               'огюмизде' => 'ögümizde', 'огюмиздеки' => 'ögümizdeki', 'огюмни' => 'ögümni',
-               'огюнде' => 'ögünde', 'огюндеки' => 'ögündeki', 'огюндекиси' => 'ögündekisi',
-               'огюнден' => 'ögünden', 'огюне' => 'ögüne', 'огюнъизде' => 'ögüñizde', 'огютини' => 'ögütini',
-               'огютлерини' => 'ögütlerini', 'озю' => 'özü', 'озюм' => 'özüm', 'озюмден' => 'özümden',
-               'озюме' => 'özüme', 'озюмизни' => 'özümizni', 'озюмизнинъ' => 'özümizniñ',
-               'озюмизнинъки' => 'özümizniñki', 'озюмнен' => 'özümnen', 'озюмни' => 'özümni',
-               'озюмнинъ' => 'özümniñ', 'озюнде' => 'özünde', 'озюнден' => 'özünden', 'озюне' => 'özüne',
-               'озюнен' => 'özünen', 'озюни' => 'özüni', 'озюнинъ' => 'özüniñ', 'озюнинъкими' => 'özüniñkimi',
-               'озюнъ' => 'özüñ', 'озюнъе' => 'özüñe', 'озюнъиз' => 'özüñiz', 'озюнъиздеки' => 'özüñizdeki',
-               'озюнъни' => 'özüñni', 'оксюз' => 'öksüz', 'окюндим' => 'ökündim', 'ольдюрип' => 'öldürip',
-               'ольдюрмек' => 'öldürmek', 'ольдюрювде' => 'öldürüvde', 'ольчюде' => 'ölçüde', 'олюм' => 'ölüm',
-               'олюмден' => 'ölümden', 'олюмлер' => 'ölümler', 'омюр' => 'ömür', 'омюрге' => 'ömürge',
-               'омюри' => 'ömüri', 'опькеленюв' => 'öpkelenüv', 'орьтилюви' => 'örtilüvi', 'орьтюли' => 'örtüli',
-               'орюли' => 'örüli', 'орюлип' => 'örülip', 'осюв' => 'ösüv', 'осюмлик' => 'ösümlik',
-               'отькерювни' => 'ötkerüvni', 'отькюр' => 'ötkür', 'офицери' => 'ofitseri',
-               'офицерим' => 'ofitserim', 'офицерлер' => 'ofitserler', 'пальтосыны' => 'paltosını',
-               'пальтосынынъ' => 'paltosınıñ', 'пекинюв' => 'pekinüv', 'пекитювнинъ' => 'pekitüvniñ',
-               'пиширюв' => 'pişirüv', 'повидло' => 'povidlo', 'полис' => 'polis', 'полициясы' => 'politsiyası',
-               'помещик' => 'pomeşçik', 'потюк' => 'potük', 'потюклеринен' => 'potüklerinen',
-               'пулемёт' => 'pülemöt', 'пулемётларны' => 'pülemötlarnı', 'режиссёр' => 'rejissör',
-               'ролюнде' => 'rolünde', 'севастопольнинъ' => 'sevastopolniñ', 'сёгди' => 'sögdi', 'сёз' => 'söz',
-               'сёзлер' => 'sözler', 'сёзлери' => 'sözleri', 'сёзлерим' => 'sözlerim',
-               'сёзлеримден' => 'sözlerimden', 'сёзлериме' => 'sözlerime', 'сёзлеримни' => 'sözlerimni',
-               'сёзлеримнинъ' => 'sözlerimniñ', 'сёзлеринде' => 'sözlerinde', 'сёзлерине' => 'sözlerine',
-               'сёзлерини' => 'sözlerini', 'сёзлерининъ' => 'sözleriniñ', 'сёзлеринъиз' => 'sözleriñiz',
-               'сёзлеринъизни' => 'sözleriñizni', 'сёзлернен' => 'sözlernen', 'сёзлерни' => 'sözlerni',
-               'сёзлернинъ' => 'sözlerniñ', 'сёзнен' => 'söznen', 'сёзни' => 'sözni', 'сёзчиклер' => 'sözçikler',
-               'сёзчиклерден' => 'sözçiklerden', 'сёзю' => 'sözü', 'сёзюмен' => 'sözümen',
-               'сёзюмнинъ' => 'sözümniñ', 'сёзюне' => 'sözüne', 'сёзюни' => 'sözüni', 'сёзюнинъ' => 'sözüniñ',
-               'сёйле' => 'söyle', 'сёйлегенде' => 'söylegende', 'сёйлегенлеринден' => 'söylegenlerinden',
-               'сёйледи' => 'söyledi', 'сёйлей' => 'söyley', 'сёйленди' => 'söylendi',
-               'сёйленмеге' => 'söylenmege', 'сёйленмекте' => 'söylenmekte', 'сёйленъиз' => 'söyleñiz',
-               'сёнген' => 'söngen', 'сёнди' => 'söndi', 'сёндюрди' => 'söndürdi',
-               'сёндюрильген' => 'söndürilgen', 'сёндюрип' => 'söndürip', 'сентябрьнинъ' => 'sentâbrniñ',
-               'сергюзешт' => 'sergüzeşt', 'сергюзештлерни' => 'sergüzeştlerni',
-               'ставропольге' => 'stavropolge', 'сулькевич' => 'sulkeviç', 'сурьат' => 'surat',
-               'суфлёр' => 'suflör', 'сюеги' => 'süyegi', 'сюеклерге' => 'süyeklerge',
-               'сюйрекледи' => 'süyrekledi', 'сюйреле' => 'süyrele', 'сюйрен' => 'süyren',
-               'сюйренге' => 'süyrenge', 'сюйренде' => 'süyrende', 'сюйреп' => 'süyrep', 'сюйрю' => 'süyrü',
-               'сюкюнет' => 'sükünet', 'сюкюнети' => 'süküneti', 'сюкюнетте' => 'sükünette', 'сюкют' => 'süküt',
-               'сюляле' => 'sülâle', 'сюрген' => 'sürgen', 'сюрди' => 'sürdi', 'сюрмеди' => 'sürmedi',
-               'сюрюльмеген' => 'sürülmegen', 'сют' => 'süt', 'тебессюм' => 'tebessüm', 'тёкип' => 'tökip',
-               'тёкти' => 'tökti', 'тёкюльген' => 'tökülgen', 'тёкюльди' => 'töküldi',
-               'тёкюндиси' => 'tökündisi', 'тёле' => 'töle', 'тёледим' => 'töledim', 'телюке' => 'telüke',
-               'телюкели' => 'telükeli', 'тенеффюс' => 'teneffüs', 'тенеффюслер' => 'teneffüsler',
-               'тёпеге' => 'töpege', 'тёпелери' => 'töpeleri', 'тёпелерине' => 'töpelerine',
-               'тёпели' => 'töpeli', 'тёпеси' => 'töpesi', 'тёпесинден' => 'töpesinden',
-               'тёпесини' => 'töpesini', 'тёрге' => 'törge', 'тёрде' => 'törde', 'тёрдеки' => 'tördeki',
-               'тёрюне' => 'törüne', 'тешеббюсим' => 'teşebbüsim', 'тёшегинден' => 'töşeginden',
-               'тёшегине' => 'töşegine', 'тёшек' => 'töşek', 'тешеккюр' => 'teşekkür',
-               'тешеккюрлер' => 'teşekkürler', 'тёшекни' => 'töşekni', 'тёшектен' => 'töşekten',
-               'тёшели' => 'töşeli', 'тёшемек' => 'töşemek', 'тёшеп' => 'töşep', 'теэссюф' => 'teessüf',
-               'тюбю' => 'tübü', 'тюбюнде' => 'tübünde', 'тюбюндеки' => 'tübündeki', 'тюз' => 'tüz',
-               'тюзельгенге' => 'tüzelgenge', 'тюзельтмек' => 'tüzeltmek', 'тюземликлер' => 'tüzemlikler',
-               'тюзетип' => 'tüzetip', 'тюзетирим' => 'tüzetirim', 'тюзеткен' => 'tüzetken',
-               'тюзетмеге' => 'tüzetmege', 'тюзетмесенъ' => 'tüzetmeseñ', 'тюзетти' => 'tüzetti',
-               'тюзетюв' => 'tüzetüv', 'тюкенмез' => 'tükenmez', 'тюкюриктен' => 'tükürikten',
-               'тюкян' => 'tükân', 'тюкяны' => 'tükânı', 'тюкянында' => 'tükânında', 'тюм' => 'tüm',
-               'тюневин' => 'tünevin', 'тюневинки' => 'tünevinki', 'тюпсюз' => 'tüpsüz', 'тюрк' => 'türk',
-               'тюрклернинъ' => 'türklerniñ', 'тюркнинъ' => 'türkniñ', 'тюркче' => 'türkçe', 'тюркю' => 'türkü',
-               'тюркюлерини' => 'türkülerini', 'тюркюнинъ' => 'türküniñ', 'тюрлю' => 'türlü',
-               'тюртип' => 'türtip', 'тюрттинъиз' => 'türttiñiz', 'тютемекте' => 'tütemekte', 'тютюн' => 'tütün',
-               'тютюнджи' => 'tütünci', 'тюфеги' => 'tüfegi', 'тюфегини' => 'tüfegini', 'тюфек' => 'tüfek',
-               'тюфеклеринен' => 'tüfeklerinen', 'тюфеклернен' => 'tüfeklernen', 'тюфеклерни' => 'tüfeklerni',
-               'тюфекнен' => 'tüfeknen', 'тюфексиз' => 'tüfeksiz', 'тюш' => 'tüş', 'тюше' => 'tüşe',
-               'тюшеджек' => 'tüşecek', 'тюшеджексинъми' => 'tüşeceksiñmi', 'тюшем' => 'tüşem',
-               'тюшип' => 'tüşip', 'тюшкен' => 'tüşken', 'тюшкенде' => 'tüşkende', 'тюшкенлер' => 'tüşkenler',
-               'тюшмеге' => 'tüşmege', 'тюшмейим' => 'tüşmeyim', 'тюшмейлер' => 'tüşmeyler',
-               'тюшмек' => 'tüşmek', 'тюшмекте' => 'tüşmekte', 'тюшмеси' => 'tüşmesi', 'тюшсе' => 'tüşse',
-               'тюшти' => 'tüşti', 'тюштик' => 'tüştik', 'тюштилер' => 'tüştiler', 'тюштими' => 'tüştimi',
-               'тюштинъиз' => 'tüştiñiz', 'тюшювден' => 'tüşüvden', 'тюшюджек' => 'tüşücek',
-               'тюшюнген' => 'tüşüngen', 'тюшюнгендже' => 'tüşüngence', 'тюшюндже' => 'tüşünce',
-               'тюшюнджеге' => 'tüşüncege', 'тюшюнджелер' => 'tüşünceler', 'тюшюнджелери' => 'tüşünceleri',
-               'тюшюнджелерим' => 'tüşüncelerim', 'тюшюнджели' => 'tüşünceli', 'тюшюнджеси' => 'tüşüncesi',
-               'тюшюнди' => 'tüşündi', 'тюшюндим' => 'tüşündim', 'тюшюне' => 'tüşüne',
-               'тюшюнелер' => 'tüşüneler', 'тюшюнесинъиз' => 'tüşünesiñiz', 'тюшюнип' => 'tüşünip',
-               'тюшюнмеге' => 'tüşünmege', 'тюшюнмезсинъ' => 'tüşünmezsiñ', 'тюшюнмей' => 'tüşünmey',
-               'тюшюнмемек' => 'tüşünmemek', 'тюшюргенлер' => 'tüşürgenler', 'тюшюрди' => 'tüşürdi',
-               'тюшюрдик' => 'tüşürdik', 'тюшюре' => 'tüşüre', 'тюшюрип' => 'tüşürip', 'тюшюрмек' => 'tüşürmek',
-               'уджюм' => 'ücüm', 'удюр' => 'üdür', 'узюле' => 'üzüle', 'узюлип' => 'üzülip',
-               'узюльгенини' => 'üzülgenini', 'узюльди' => 'üzüldi', 'уйрюлип' => 'üyrülip',
-               'укюмет' => 'ükümet', 'укюмети' => 'ükümeti', 'укюметими' => 'ükümetimi',
-               'укюметимиз' => 'ükümetimiz', 'укюметини' => 'ükümetini', 'укюметининъ' => 'ükümetiniñ',
-               'укюметке' => 'ükümetke', 'укюметкеми' => 'ükümetkemi', 'укюметми' => 'ükümetmi',
-               'укюметнинъ' => 'ükümetniñ', 'укюметтен' => 'ükümetten', 'укюмран' => 'ükümran',
-               'улькюн' => 'ülkün', 'умюдим' => 'ümüdim', 'умют' => 'ümüt', 'умютлери' => 'ümütleri',
-               'умютсизден' => 'ümütsizden', 'усть' => 'üst', 'устьке' => 'üstke', 'устьлеринде' => 'üstlerinde',
-               'устьлериндеки' => 'üstlerindeki', 'устьлерине' => 'üstlerine', 'устьлерини' => 'üstlerini',
-               'устюрткъа' => 'üsturtqa', 'усьнюхаткъа' => 'üsnühatqa', 'усьнюхаты' => 'üsnühatı',
-               'усьтю' => 'üstü', 'усьтюмде' => 'üstümde', 'усьтюмдеки' => 'üstümdeki', 'усьтюме' => 'üstüme',
-               'усьтюнде' => 'üstünde', 'усьтюндеки' => 'üstündeki', 'усьтюндемиз' => 'üstündemiz',
-               'усьтюне' => 'üstüne', 'усьтюни' => 'üstüni', 'усьтюнлик' => 'üstünlik',
-               'усьтюнъизге' => 'üstüñizge', 'утёкунь' => 'ütökün', 'уфюрди' => 'üfürdi', 'учю' => 'üçü',
-               'учюмиз' => 'üçümiz', 'учюн' => 'üçün', 'учюнджи' => 'üçünci', 'учюнджисининъ' => 'üçüncisiniñ',
-               'ушюй' => 'üşüy', 'ушюмез' => 'üşümez', 'ушюмезсинъ' => 'üşümezsiñ',
-               'факультетинде' => 'fakultetinde', 'факультетине' => 'fakultetine',
-               'февральнинъ' => 'fevralniñ', 'харьковдаки' => 'harkovdaki', 'харьковдан' => 'harkovdan',
-               'чёкти' => 'çökti', 'чёкюрли' => 'çökürli', 'чёкюч' => 'çöküç', 'чёллюкке' => 'çöllükke',
-               'чёль' => 'çöl', 'чёльде' => 'çölde', 'чёльмек' => 'çölmek', 'чёткю' => 'çötkü',
-               'чёчамийлер' => 'çöçamiyler', 'чюнки' => 'çünki', 'чюрюди' => 'çürüdi', 'чюрюк' => 'çürük',
-               'шукюр' => 'şükür', 'шукюрлер' => 'şükürler', 'этюв' => 'etüv', 'этювден' => 'etüvden',
-               'этюви' => 'etüvi', 'этюдлар' => 'etüdlar', 'юзден' => 'yüzden', 'юзлеп' => 'yüzlep',
-               'юзлерини' => 'yüzlerini', 'юзлернен' => 'yüzlernen', 'юзлюги' => 'yüzlügi',
-               'юзлюкке' => 'yüzlükke', 'юзю' => 'yüzü', 'юзюм' => 'yüzüm', 'юзюме' => 'yüzüme',
-               'юзюмен' => 'yüzümen', 'юзюмни' => 'yüzümni', 'юзюнде' => 'yüzünde', 'юзюни' => 'yüzüni',
-               'юзюнинъ' => 'yüzüniñ', 'юзюнъ' => 'yüzüñ', 'юзюнъизге' => 'yüzüñizge', 'юклю' => 'yüklü',
-               'юксельтюв' => 'yükseltüv', 'юньлю' => 'yünlü', 'юньлюдже' => 'yünlüce',
-               'юртсеверлик' => 'yurtseverlik', 'юртюде' => 'yürtüde', 'юрьтю' => 'yürtü',
-               'юрьтюге' => 'yürtüge', 'юрьтюнинъ' => 'yürtüniñ', 'юрюльсе' => 'yürülse', 'юрюнъиз' => 'yürüñiz',
-               'юрюш' => 'yürüş', 'юрюши' => 'yürüşi', 'юрюшим' => 'yürüşim', 'юрюшини' => 'yürüşini',
-               'юрюшнен' => 'yürüşnen', 'юрюшни' => 'yürüşni',
        ];
 
-       # map Cyrillic to Latin and back, whole word match only
+       # map Cyrillic to Latin and back, simple string match only (no regex)
        # no variants: map exactly as is
-       # items with capture group refs (e.g., $1) are only mapped from the
-       # regex to the reference
        private $exactCaseMappings = [
                # аббревиатуры
                # abbreviations
-               'ОБСЕ' => 'OBSE', 'КъМДж' => 'QMC', 'КъАЭ' => 'QAE', 'ГъСМК' => 'ĞSMK', 'ШСДжБ' => 'ŞSCB',
-               'КъМШСДж' => 'QMŞSC', 'КъДМПУ' => 'QDMPU', 'КъМПУ' => 'QMPU', 'КъЮШ' => 'QYŞ', 'ЮШ' => 'YŞ',
+               'ОБСЕ' => 'OBSE', 'КъМДж' => 'QMC', 'КъДж' => 'QC', 'КъАЭ' => 'QAE', 'ГъСМК' => 'ĞSMK',
+               'ШСДжБ' => 'ŞSCB', 'КъМШСДж' => 'QMŞSC', 'КъАССР' => 'QASSR', 'КъДМПУ' => 'QDMPU',
+               'КъМПУ' => 'QMPU',
        ];
 
        # map Cyrillic to Latin and back, match end of word
@@ -517,10 +356,12 @@ class CrhExceptions {
                # originally C2L
                'иаль' => 'ial', 'нуль' => 'nul', 'кой' => 'köy', 'койнинъ' => 'köyniñ', 'койни' => 'köyni',
                'койге' => 'köyge', 'койде' => 'köyde', 'койдеки' => 'köydeki', 'койден' => 'köyden',
-               'козь' => 'köz',
+               'козь' => 'köz', '-юнджи' => '-ünci', '-юнджиде' => '-üncide', '-юнджиден' => '-ünciden',
 
                # originally L2C, here swapped
-               'етсин' => 'etsin',
+               'етсин' => 'etsin', 'льная' => 'lnaya', 'льное' => 'lnoye', 'льный' => 'lnıy', 'льний' => 'lniy',
+               'льская' => 'lskaya', 'льский' => 'lskiy', 'льское' => 'lskoye', 'ополь' => 'opol',
+               'щее' => 'şçeye', 'щий' => 'şçiy', 'щая' => 'şçaya', 'цепс' => 'tseps',
 
        ];
 
@@ -533,15 +374,18 @@ class CrhExceptions {
                'буюк([^ъ])' => 'büyük$1', 'бую([гдйлмнпрстчшc])(и)' => 'büyü$1$2',
                'буют([^ыа])' => 'büyüt$1', 'джонк([^ъ])' => 'cönk$1', 'коюм' => 'köyüm', 'коюнъ' => 'köyüñ',
                'коюн([ди])' => 'köyün$1', 'куе' => 'küye', 'куркке' => 'kürkke', 'куркни' => 'kürkni',
-               'куркте' => 'kürkte', 'куркчи' => 'kürkçi', 'куркчю' => 'kürkçü',
+               'куркте' => 'kürkte', 'куркчю' => 'kürkçü', 'кою' => 'köyü',
+               'жизнь' => 'jizn',
 
                # арабизмы на муи- муэ- / Arabic муи- муэ-
                'му([иэИЭ])' => 'mü$1',
 
                # originally L2C, here swapped
-               'итъаль' => 'ital',
                'роль$1' => 'rol([^ü])',
-               'усть$1' => 'üst([knt])',
+               'усть$1' => 'üst([^ü])',
+
+               # more prefixes
+               'ком-кок' => 'köm-kök',
 
        ];
 
@@ -555,8 +399,68 @@ class CrhExceptions {
                        # относятся ко всему слову #
                        # whole words              #
                        ############################
-                       '/\b([34])(\-)юнджи\b/u' => '$1$2ünci',
-                       '/\b([34])(\-)ЮНДЖИ\b/u' => '$1$2ÜNCİ',
+
+                       // TODO: refactor upper/lower/first capital whole words without
+                       // regexes into simpler list
+
+                       '/\bКъЮШ\b/u' => 'QYŞ',
+                       '/\bЮШ\b/u' => 'YŞ',
+
+                       '/\bкок\b/u' => 'kök',
+                       '/\bКок\b/u' => 'Kök',
+                       '/\bКОК\b/u' => 'KÖK',
+                       '/\bком-кок\b/u' => 'köm-kök',
+                       '/\bКом-кок\b/u' => 'Köm-kök',
+                       '/\bКОМ-КОК\b/u' => 'KÖM-KÖK',
+
+                       '/\bкоп\b/u' => 'köp',
+                       '/\bКоп\b/u' => 'Köp',
+                       '/\bКОП\b/u' => 'KÖP',
+
+                       '/\bкурк\b/u' => 'kürk',
+                       '/\bКурк\b/u' => 'Kürk',
+                       '/\bКУРК\b/u' => 'KÜRK',
+
+                       '/\bог\b/u' => 'ög',
+                       '/\bОг\b/u' => 'Ög',
+                       '/\bОГ\b/u' => 'ÖG',
+
+                       '/\bюрип\b/u' => 'yürip',
+                       '/\bЮрип\b/u' => 'Yürip',
+                       '/\bЮРИП\b/u' => 'YÜRİP',
+
+                       '/\bюз\b/u' => 'yüz',
+                       '/\bЮз\b/u' => 'Yüz',
+                       '/\bЮЗ\b/u' => 'YÜZ',
+
+                       '/\bюк\b/u' => 'yük',
+                       '/\bЮк\b/u' => 'Yük',
+                       '/\bЮК\b/u' => 'YÜK',
+
+                       '/\bбуюп\b/u' => 'büyüp',
+                       '/\bБуюп\b/u' => 'Büyüp',
+                       '/\bБУЮП\b/u' => 'BÜYÜP',
+
+                       '/\bбуюк\b/u' => 'büyük',
+                       '/\bБуюк\b/u' => 'Büyük',
+                       '/\bБУЮК\b/u' => 'BÜYÜK',
+
+                       '/\bджонк\b/u' => 'cönk',
+                       '/\bДжонк\b/u' => 'Cönk',
+                       '/\bДЖОНК\b/u' => 'CÖNK',
+                       '/\bджонкю\b/u' => 'cönkü',
+                       '/\bДжонкю\b/u' => 'Cönkü',
+                       '/\bДЖОНКЮ\b/u' => 'CÖNKÜ',
+
+                       '/\bустке\b/u' => 'üstke',
+                       '/\bУстке\b/u' => 'Üstke',
+                       '/\bУСТКЕ\b/u' => 'ÜSTKE',
+                       '/\bустте\b/u' => 'üstte',
+                       '/\bУстте\b/u' => 'Üstte',
+                       '/\bУСТТЕ\b/u' => 'ÜSTTE',
+                       '/\bусттен\b/u' => 'üstten',
+                       '/\bУсттен\b/u' => 'Üstten',
+                       '/\bУСТТЕН\b/u' => 'ÜSTTEN',
 
                        # отдельно стоящие Ё и Я
                        # stand-alone Ё and Я
@@ -570,6 +474,16 @@ class CrhExceptions {
                        '/\bКъЮШн/u' => 'QYŞn',
                        '/\bЮШн/u' => 'YŞn',
 
+                       # need to convert digraphs (гъ, къ, нъ, дж) now to match patterns
+                       '/гъ/u' => 'ğ',
+                       '/Г[ъЪ]/u' => 'Ğ',
+                       '/къ/u' => 'q',
+                       '/К[ъЪ]/u' => 'Q',
+                       '/нъ/u' => 'ñ',
+                       '/Н[ъЪ]/u' => 'Ñ',
+                       '/дж/u' => 'c',
+                       '/Д[жЖ]/u' => 'C',
+
                        # о => ö
                        '/\b(['.Crh::C_M_CONS.'])о(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => '$1ö$2$3$4',
                        '/\bо(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => 'ö$1$2$3',
@@ -662,63 +576,101 @@ class CrhExceptions {
                ];
 
                $this->Latn2CyrlRegexes = [
+
+                       // TODO: refactor upper/lower/first capital whole words without
+                       // regexes into simpler list
+
+                       '/\ban\b/u' => 'ань',
+                       '/\bAn\b/u' => 'Ань',
+                       '/\bAN\b/u' => 'АНЬ',
+                       '/\bange\b/u' => 'аньге',
+                       '/\bAnge\b/u' => 'Аньге',
+                       '/\bANGE\b/u' => 'АНЬГЕ',
+                       '/\bande\b/u' => 'аньде',
+                       '/\bAnde\b/u' => 'Аньде',
+                       '/\bANDE\b/u' => 'АНЬДЕ',
+                       '/\banki\b/u' => 'аньки',
+                       '/\bAnki\b/u' => 'Аньки',
+                       '/\bANKİ\b/u' => 'АНЬКИ',
+                       '/\bderal\b/u' => 'деръал',
+                       '/\bDeral\b/u' => 'Деръал',
+                       '/\bDERAL\b/u' => 'ДЕРЪАЛ',
+                       '/\bkör\b/u' => 'кёр',
+                       '/\bKör\b/u' => 'Кёр',
+                       '/\bKÖR\b/u' => 'КЁР',
+                       '/\bmer\b/u' => 'мэр',
+                       '/\bMer\b/u' => 'Мэр',
+                       '/\bMER\b/u' => 'МЭР',
+
+                       '/\bджонк/u' => 'cönk',
+                       '/\bДжонк/u' => 'Cönk',
+                       '/\bДЖОНК/u' => 'CÖNK',
+
+                       '/\bкуркчи/u' => 'kürkçi',
+                       '/\bКуркчи/u' => 'Kürkçi',
+                       '/\bКУРКЧИ/u' => 'KÜRKÇI',
+
                        # буква Ё - первый заход
                        # расставляем Ь после согласных
-                       '/^([yY])ö(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|$)/u' => '$1ö$2ь$3',
-                       '/^([yY])Ö(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|$)/u' => '$1Ö$2Ь$3',
-                       '/^AQŞ(['.Crh::WORD_ENDS.'ngd])/u' => 'АКъШ$1',
+                       '/\b([yY])ö(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|\b)/u' => '$1ö$2ь$3',
+                       '/\b([yY])Ö(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|\b)/u' => '$1Ö$2Ь$3',
+                       '/\bAQŞ([^AEI]|\b)/u' => 'АКъШ$1',
 
                        # буква Ю - первый заход
                        # расставляем Ь после согласных
-                       '/^([yY])ü(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|$)/u' => '$1ü$2ь$3',
-                       '/^([yY])Ü(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|$)/u' => '$1Ü$2Ь$3',
+                       '/\b([yY])ü(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|\b)/u' => '$1ü$2ь$3',
+                       '/\b([yY])Ü(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|\b)/u' => '$1Ü$2Ь$3',
 
-                       '/^([bcgkpşBCGKPŞ])ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1ö$2ь$3',
-                       '/^([bcgkpşBCGKPŞ])Ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1Ö$2Ь$3',
-                       '/^([bcgkpşBCGKPŞ])Ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1Ö$2Ь$3',
-                       '/^([bcgkpşBCGKPŞ])ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1ü$2ь$3',
-                       '/^([bcgkpşBCGKPŞ])Ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1Ü$2Ь$3',
-                       '/^([bcgkpşBCGKPŞ])Ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1Ü$2Ь$3',
+                       '/\b([bcgkpşBCGKPŞ])ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|\b)/u' => '$1ö$2ь$3',
+                       '/\b([bcgkpşBCGKPŞ])Ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|\b)/u' => '$1Ö$2Ь$3',
+                       '/\b([bcgkpşBCGKPŞ])Ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|\b)/u' => '$1Ö$2Ь$3',
+                       '/\b([bcgkpşBCGKPŞ])ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|\b)/u' => '$1ü$2ь$3',
+                       '/\b([bcgkpşBCGKPŞ])Ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|\b)/u' => '$1Ü$2Ь$3',
+                       '/\b([bcgkpşBCGKPŞ])Ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|\b)/u' => '$1Ü$2Ь$3',
 
                         # ö и ü в начале слова
                         # случаи, когда нужен Ь
-                       '/^ö(['.Crh::L_N_CONS.'pP])(['.Crh::L_CONS.']|$)/u' => 'ö$1ь$2',
-                       '/^Ö(['.Crh::L_N_CONS_LC.'p])(['.Crh::L_CONS.']|$)/u' => 'Ö$1ь$2',
-                       '/^Ö(['.Crh::L_N_CONS_UC.'P])(['.Crh::L_CONS.']|$)/u' => 'Ö$1Ь$2',
-                       '/^ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => 'ü$1ь$2',
-                       '/^Ü(['.Crh::L_N_CONS_LC.'])(['.Crh::L_CONS.']|$)/u' => 'Ü$1ь$2',
-                       '/^Ü(['.Crh::L_N_CONS_UC.'])(['.Crh::L_CONS.']|$)/u' => 'Ü$1Ь$2',
-
-                       '/ts$/u' => 'ц',
-                       '/şç$/u' => 'щ',
-                       '/Ş[çÇ]$/u' => 'Щ',
-                       '/T[sS]$/u' => 'Ц',
+                       '/\bö(['.Crh::L_N_CONS.'pP])(['.Crh::L_CONS.']|\b)/u' => 'ö$1ь$2',
+                       '/\bÖ(['.Crh::L_N_CONS_LC.'p])(['.Crh::L_CONS.']|\b)/u' => 'Ö$1ь$2',
+                       '/\bÖ(['.Crh::L_N_CONS_UC.'P])(['.Crh::L_CONS.']|\b)/u' => 'Ö$1Ь$2',
+                       '/\bü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|\b)/u' => 'ü$1ь$2',
+                       '/\bÜ(['.Crh::L_N_CONS_LC.'])(['.Crh::L_CONS.']|\b)/u' => 'Ü$1ь$2',
+                       '/\bÜ(['.Crh::L_N_CONS_UC.'])(['.Crh::L_CONS.']|\b)/u' => 'Ü$1Ь$2',
+
+                       '/ts\b/u' => 'ц',
+                       '/şç\b/u' => 'щ',
+                       '/Ş[çÇ]\b/u' => 'Щ',
+                       '/T[sS]\b/u' => 'Ц',
 
                        # Ь после Л
                        # add Ь after Л
-                       '/(['.Crh::L_F.'])l(['.Crh::L_CONS_LC.']|$)/u' => '$1ль$2',
-                       '/(['.Crh::L_F_UC.'])L(['.Crh::L_CONS.']|$)/u' => '$1ЛЬ$2',
+                       '/(['.Crh::L_F.'])l(['.Crh::L_CONS_LC.']|\b)/u' => '$1ль$2',
+                       '/(['.Crh::L_F_UC.'])L(['.Crh::L_CONS.']|\b)/u' => '$1ЛЬ$2',
+
+                       '/etsin\b/u' => 'етсин',
+                       '/Etsin\b/u' => 'Етсин',
+                       '/ETSİN\b/u' => 'ЕТСИН',
 
                        # относятся к началу слова
-                       '/^ts/u' => 'ц',
-                       '/^T[sS]/u' => 'Ц',
+                       '/\bts/u' => 'ц',
+                       '/\bT[sS]/u' => 'Ц',
 
-                       '/^şç/u' => 'щ',
-                       '/^Ş[çÇ]/u' => 'Щ',
+                       '/\bşç/u' => 'щ',
+                       '/\bŞ[çÇ]/u' => 'Щ',
 
                        # Э
-                       '/(^|['.Crh::L_VOW.'аеэяАЕЭЯ])e/u' => '$1э',
-                       '/(^|['.Crh::L_VOW_UC.'АЕЭЯ])E/u' => '$1Э',
+                       '/(\b|['.Crh::L_VOW.'аеэяАЕЭЯ])e/u' => '$1э',
+                       '/(\b|['.Crh::L_VOW_UC.'АЕЭЯ])E/u' => '$1Э',
 
-                       '/^(['.Crh::L_M_CONS.'])ö/u' => '$1о',
-                       '/^(['.Crh::L_M_CONS.'])Ö/u' => '$1О',
-                       '/^(['.Crh::L_M_CONS.'])ü/u' => '$1у',
-                       '/^(['.Crh::L_M_CONS.'])Ü/u' => '$1У',
+                       '/\b(['.Crh::L_M_CONS.'])ö/u' => '$1о',
+                       '/\b(['.Crh::L_M_CONS.'])Ö/u' => '$1О',
+                       '/\b(['.Crh::L_M_CONS.'])ü/u' => '$1у',
+                       '/\b(['.Crh::L_M_CONS.'])Ü/u' => '$1У',
 
-                       '/^ö/u' => 'о',
-                       '/^Ö/u' => 'О',
-                       '/^ü/u' => 'у',
-                       '/^Ü/u' => 'У',
+                       '/\bö/u' => 'о',
+                       '/\bÖ/u' => 'О',
+                       '/\bü/u' => 'у',
+                       '/\bÜ/u' => 'У',
 
                        # некоторые исключения
                        # some exceptions
@@ -780,13 +732,18 @@ class CrhExceptions {
                        '/[ьЬ]([aA])/u' => '$1',
 
                        # дж
-                       '/C(['.Crh::L_UC.Crh::C_UC.'Ъ])/u' => 'ДЖ$1',
+                       '/C(['.Crh::L_UC.Crh::C_UC.'АЕЁЙОУЭЮЯ])/u' => 'ДЖ$1',
+                       '/(['.Crh::L_UC.Crh::C_UC.'АЕЁЙОУЭЮЯ])C/u' => '$1ДЖ',
 
                        # гъ, къ, нъ
-                       # гъ, къ, нъ
-                       '/Ğ(['.Crh::L_UC.Crh::C_UC.'Ъ])/u' => 'ГЪ$1',
-                       '/Q(['.Crh::L_UC.Crh::C_UC.'Ъ])/u' => 'КЪ$1',
-                       '/Ñ(['.Crh::L_UC.Crh::C_UC.'Ъ])/u' => 'НЪ$1',
+                       '/Ğ(['.Crh::L_UC.Crh::C_UC.'])/u' => 'ГЪ$1',
+                       '/(['.Crh::L_UC.Crh::C_UC.'Ъ])Ğ/u' => '$1ГЪ',
+
+                       '/Q(['.Crh::L_UC.Crh::C_UC.'])/u' => 'КЪ$1',
+                       '/(['.Crh::L_UC.Crh::C_UC.'Ъ])Q/u' => '$1КЪ',
+
+                       '/Ñ(['.Crh::L_UC.Crh::C_UC.'])/u' => 'НЪ$1',
+                       '/(['.Crh::L_UC.Crh::C_UC.'Ъ])Ñ/u' => '$1НЪ',
 
                ];
        }
index 710d6be..77b9375 100644 (file)
@@ -48,6 +48,7 @@ class Names {
        public static $names = [
                'aa' => 'Qafár af', # Afar
                'ab' => 'Аҧсшәа', # Abkhaz
+               'abs' => 'bahasa ambon', # Ambonese Malay, T193566
                'ace' => 'Acèh', # Aceh
                'ady' => 'адыгабзэ', # Adyghe
                'ady-cyrl' => 'адыгабзэ', # Adyghe
index 7a2ac59..b08ae3a 100644 (file)
        "mergelog": "Peugabông log",
        "revertmerge": "Hana jadèh peugabông",
        "history-title": "Riwayat geunantoë nibak \"$1\"",
+       "difference-title": "Bida antara revisi nibak \"$1\"",
        "lineno": "Baréh $1:",
        "compareselectedversions": "Peubandéng curak teupiléh",
        "editundo": "pubateuë",
        "diff-empty": "(Hana bida)",
+       "diff-multi-sameuser": "({{PLURAL:$1|Saboh revisi antara|$1 revisi antara}} lé ureueng ngui nyang saban hana geupeudeuih)",
        "searchresults": "Hasé mita",
        "searchresults-title": "Hasé mita keu \"$1\"",
        "notextmatches": "Hana naseukah laman nyang pah",
        "pager-older-n": "{{PLURAL:$1|1 leubèh awai|$1 leubèh awai}}",
        "booksources": "Nè kitab",
        "booksources-search-legend": "Mita bak nè kitab",
+       "booksources-search": "Mita",
        "specialloguserlabel": "Ureuëng ngui:",
        "speciallogtitlelabel": "Sasaran (judu atawa {{ns:ureueng ngui}}:nan ureueng ngui keu ureueng ngui)",
        "log": "Log",
        "tag-filter-submit": "Saréng",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Tag}}]]: $2)",
        "logentry-delete-delete": "$1 {{GENDER:$2|geusampôh}} miëng $3",
+       "logentry-move-move": "$1 {{GENDER:$2|geupinah}} mieng $3 u $4",
        "logentry-newusers-create": "$1 {{GENDER:$2|geupeugöt}} akun ureuëng ngui",
        "logentry-upload-upload": "$1 {{GENDER:$2|geupasoe}} $3",
        "searchsuggest-search": "Mita {{SITENAME}}",
index 4f81c3a..1b15090 100644 (file)
        "compareselectedversions": "Seçilən versiyaları müqayisə et",
        "showhideselectedversions": "Seçilən versiyaları göstər/gizlə",
        "editundo": "əvvəlki halına qaytar",
+       "diff-empty": "(Fərqli deyil)",
        "diff-multi-sameuser": "(Eyni istifadəçi tərəfindən edilmiş {{PLURAL:$1|bir dəyişiklik|$1 bir neçə dəyişiklik}} göstərilmir)",
        "diff-multi-manyusers": "({{PLURAL:$2|Bir istifadəçi|$2 istifadəçi}} tərəfindən edilən {{PLURAL:$1|bir ara redaktə|$1 ara redaktə}} göstərilmir)",
        "difference-missing-revision": "Səhifənin  {{PLURAL:$2|bu versiyasının|$2 versiyalarının}} müqayisəsi ($1) tapılmadı.\nBu xəta adətən, köhnəlmiş səhifələrin müqayisə versiyalarından keçid edildikdə baş verir.\nDaha ətraflı məlumat üçün [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} silmə qeydlərinə] baxın.",
        "search-redirect": "($1 səhifəsindən yönləndirmə)",
        "search-section": "(bölmə $1)",
        "search-category": "(kateqoriya $1)",
+       "search-file-match": "(faylın məzmunu ilə oxşardır)",
        "search-suggest": "Bəlkə, bunu nəzərdə tuturdunuz: $1",
        "search-interwiki-caption": "Qonşu layihələrdəki nəticələr",
        "search-interwiki-default": "$1 nəticələri:",
        "tooltip-pt-login": "Daxil olmanız tövsiyə olunur, amma bu məcburi tələb deyil.",
        "tooltip-pt-logout": "Sistemdən çıx",
        "tooltip-ca-talk": "Məqalə haqqındə müzakirə edib, münasibətivi bildir",
-       "tooltip-ca-edit": "Bu səhifəni redaktə edə bilərsiniz. Lütfən əvvəlcə sınaq gostərişi edin.",
+       "tooltip-ca-edit": "Bu səhifəni redaktə et",
        "tooltip-ca-addsection": "Yeni bölmə yarat",
        "tooltip-ca-viewsource": "Bu səhifə dəyişikliklərdən mühafizə olunur. Amma siz onun mətninə baxa və mətnin surətini köçürə bilərsiniz.",
        "tooltip-ca-history": "Bu səhifənin keçmiş nüsxələri.",
        "tooltip-t-recentchangeslinked": "Bu məqaləyə aid başqa səhifələrdə yeni dəyişikliklər",
        "tooltip-feed-rss": "Bu səhifə üçün RSS yayımı",
        "tooltip-feed-atom": "Bu səhifə üçün Atom yayımı",
-       "tooltip-t-contributions": "Bu istifadəçinin redaktə etdiyi səhifələrin siyahısı",
+       "tooltip-t-contributions": "{{GENDER:$1|this user}} adlı istifadəçinin redaktə etdiyi səhifələrin siyahısı",
        "tooltip-t-emailuser": "{{GENDER:$1|Bu istifadəçiyə}} e-məktub göndər",
        "tooltip-t-upload": "Yeni şəkil və ya multimedia faylı yüklə",
        "tooltip-t-specialpages": "Xüsusi səhifələrin siyahısı",
        "pageinfo-visiting-watchers": "Səhifəni izləmədə saxlayanlardan son dəyişiklikləri görənlərin sayı",
        "pageinfo-few-watchers": "$1 {{PLURAL:$1|izləyicidən|izləyicilərdən}} az",
        "pageinfo-redirects-name": "Bu səhifəyə yönləndirmələrin sayı",
+       "pageinfo-subpages-name": "Bu səhifənin alt-səhifələrinin sayı",
        "pageinfo-firstuser": "Səhifəni yaradan",
        "pageinfo-firsttime": "Səhifənin yaranma tarixi",
        "pageinfo-lastuser": "Sonuncu redaktor",
        "version-entrypoints-header-url": "URL",
        "version-libraries-library": "Kitabxana",
        "version-libraries-authors": "Müəlliflər",
+       "redirect-submit": "Keç",
+       "redirect-lookup": "Bax",
        "redirect-user": "İstifadəçi ID-si",
        "redirect-page": "Səhifənin identifikatoru",
+       "redirect-revision": "Səhifənin versiyası",
+       "redirect-file": "Fayl adı",
        "fileduplicatesearch": "Dublikat fayl axtarışı",
        "fileduplicatesearch-filename": "Fayl adı:",
        "fileduplicatesearch-submit": "Axtar",
index ce20657..5f90062 100644 (file)
@@ -44,6 +44,7 @@
        "tog-watchlisthideminor": "धियानसूची से छोट संपादन छिपावल जाय",
        "tog-watchlisthideliu": "खाता में प्रवेश भइल प्रयोगकर्ता लोग के संपादन धियानसूची से छिपावल जाय",
        "tog-watchlistreloadautomatically": "जब कौनों फिल्टर बदलल जाय तब धियानसूची ऑटोमेटिक दोबारा लोड होखे (जावास्क्रिप्ट जरूरी)",
+       "tog-watchlistunwatchlinks": "धियान में राखल जवना पन्ना सभ में बदलाव भइल बा उनहन में बिनाधियान/धियान के चीन्हा सभ ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) के सीधे जोड़ीं (एह टॉगल सुबिधा खातिर जावास्क्रिप्ट के जरूरत पड़ी)",
        "tog-watchlisthideanons": "बेनाम प्रयोगकर्ता लोग के संपादन धियानसूची से छिपावल जाय",
        "tog-watchlisthidepatrolled": "जाँचल गइल संपादन के धियानसूची से छिपावल जाय",
        "tog-watchlisthidecategorization": "पन्ना श्रेणीकरण छिपावल जाय",
        "cascadeprotected": "ए पन्ना के संपादन कइल सुरक्षित क दिहल गइल बा काहें कि ई {{PLURAL:$1|पन्ना में, जौना के|पन्ना सब में, जिन्हन के}} \"कैस्केडिंग\" (बिस्तारित) सुरक्षा चालू क के सुरक्षित कइल गइल बा, में समाइल बाटे:\n$2",
        "namespaceprotected": "रउआ के '''$1''' नामस्थान के पन्नं में सम्पादन करे के अधिकार नइखे दिहल गइल।",
        "customcssprotected": "रउआ के इ CSS पन्ना के संपादित करे के अनुमति नइखे, काहे कि इ में अन्य सदस्यं के व्यक्तिगत सेटिंग्स समाविष्ट बा।",
+       "customjsonprotected": "रउआ के एह JSON पन्ना के संपादित करे के इजाजत नइखे, काहें कि एह में दुसरे प्रयोगकर्ता ब्यक्तिगत सेटिंग सामिल बा।",
        "customjsprotected": "रउआ इ जावास्क्रिप्ट पन्ना के संपादित करे के अनुमति नइखे, काहे कि इ में अन्य सदस्यं के व्यक्तिगत सेटिंग्स समाविष्ट बा।",
        "mycustomcssprotected": "रउआ इ CSS के पन्ना के सम्पादित करे के अधिकार नइखे।",
+       "mycustomjsonprotected": "आपके एह JSON पन्ना में संपादन के इजाजत नइखे।",
        "mycustomjsprotected": "रउआ इ जावास्क्रिप्ट पन्ना के सम्पादित करे के अधिकार नइखे।",
        "myprivateinfoprotected": "रउआ लगे आपन व्यक्तिगत जानकारी बदले के अनुमति नइखे।",
        "mypreferencesprotected": "रउआ लगे आपन वरियतां ‍‍‍‍(पसंद) बदले के अधिकार नइखे।",
        "wrongpasswordempty": "गुप्तशब्द खाली बा। कृपया फिर से कोसिस करीं।",
        "passwordtooshort": "गुप्तशब्द कम से कम {{PLURAL:$1|1 अक्षर|$1 अक्षर}} के होवे के चाहीं।",
        "passwordtoolong": "गुप्तशब्द {{PLURAL:$1|$1 अक्षर}} से लमहर ना चाहीं।",
-       "passwordtoopopular": "à¤\85à¤\95à¥\8dसरहा à¤¬à¥\80à¤\9bल à¤\9cाà¤\8f à¤µà¤¾à¤²à¤¾ à¤\97à¥\81पà¥\8dतशबà¥\8dद à¤¨à¤¾ à¤\87सà¥\8dतà¥\87माल à¤¹à¥\8b à¤¸à¤\95à¥\87 à¤²à¤¾à¥¤ à¤\95à¥\8cनà¥\8bà¤\82 à¤\85à¤\89रà¥\80 à¤\96ास à¤\85लà¤\97 à¤\95िसिम à¤\95à¥\87 à¤\97à¥\81पà¥\8dतशबà¥\8dद à¤\9aà¥\81नà¥\80à¤\82।",
+       "passwordtoopopular": "à¤\85à¤\95à¥\8dसरहा à¤¬à¥\80à¤\9bल à¤\9cाà¤\8f à¤µà¤¾à¤²à¤¾ à¤\97à¥\81पà¥\8dतशबà¥\8dद à¤¨à¤¾ à¤\87सà¥\8dतà¥\87माल à¤\95à¤\87ल à¤\9cा à¤¸à¤\95à¥\87 à¤²à¤¾à¥¤ à¤\85à¤\87सन à¤\97à¥\81पà¥\8dतशबà¥\8dद à¤¬à¥\80à¤\9bà¥\80à¤\82 à¤\9cà¥\87à¤\95र à¤\85à¤\82à¤\9cाद à¤²à¤\97ावल à¤¢à¥\87र à¤\95ठिन à¤¹à¥\8bà¤\96à¥\87।",
        "password-name-match": "राउर गुप्तशब्द राउर प्रयोगकर्तानाँव से अलग होखे के चाहीं।",
        "password-login-forbidden": "इस प्रयोगकर्तानाँव आ गुप्तशब्द के प्रयोग वर्जित बा।",
        "mailmypassword": "गुप्तशब्द रिसेट करीं",
        "passwordremindertitle": "{{SITENAME}} खातिर नया अस्थायी गुप्तशब्द",
-       "passwordremindertext": "केहू (शायद रउए, $1 आइपी पता से) {{SITENAME}} ($4) पर प्रयोग खातिर नया गुप्तशब्द के निवेदन कइले बा। प्रयोगकर्ता \"$2\" खातिर एगो अस्थायी गुप्तशब्द बना दिहल गइल बा, आ ई \"$3\" बा। यदि ई रउवें चाहत रहलीं, त अब रउआँ के खाता में प्रवेश क के एगो नया गुप्तशब्द चुने के पड़ी।\nराउर अस्थायी गुप्तशब्द के अवधि {{PLURAL:$5|एक दिन|$5 दिन}} में खतम हो जाई।\n\nयदि ई निवेदन केहु अउर कइले रहल, या रउआँ के आपन पुरनका गुप्तशब्द इयाद आ गइल बा आ बदलाव नइखीं चाहत, त रउआँ ए सनेसा के अनदेखा कर सकत बानी, आ आपन पुरनका गुप्तशब्द के प्रयोग पहिले नियर कर सकत बानी।",
+       "passwordremindertext": "केहू ($1 आइपी पता से) {{SITENAME}} ($4) पर प्रयोग खातिर नया गुप्तशब्द के निवेदन कइले बा। प्रयोगकर्ता \"$2\" खातिर एगो अस्थायी गुप्तशब्द बना दिहल गइल बा, आ ई \"$3\" बा। यदि ई रउवें चाहत रहलीं, त अब रउआँ के खाता में प्रवेश क के एगो नया गुप्तशब्द चुने के पड़ी।\nराउर अस्थायी गुप्तशब्द के समयसीमा {{PLURAL:$5|एक दिन|$5 दिन}} में खतम हो जाई।\n\nयदि ई निवेदन केहु अउर कइले रहल, या रउआँ के आपन पुरनका गुप्तशब्द इयाद आ गइल बा आ बदलाव नइखीं चाहत, त रउआँ ए सनेसा के अनदेखा कर सकत बानी, आ आपन पुरनका गुप्तशब्द के प्रयोग पहिले नियर कर सकत बानी।",
        "noemail": "\"$1\" प्रयोगकर्ता खातिर कौनों ईमेल पता रिकार्ड में नइखे।",
        "noemailcreate": "रउआँ के एगो जायज ईमेल पता देवे के पड़ी।",
        "passwordsent": "\"$1\" के ईमेल पता पर एगो नया गुप्तशब्द भेज दिहल गइल बा।\nईमेल पावे के बाद कृपया दुबारा खाता में प्रवेश करीं।",
        "expansion-depth-exceeded-warning": "पन्ना अधिकतम बिस्तार गहिराई के पार क गइल",
        "parser-unstrip-loop-warning": "अनस्ट्रिप लूप पकड़ में आइल बा",
        "unstrip-depth-warning": "अनस्ट्रिप रिकर्शन सीमा पार हो गइल ($1)",
+       "unstrip-depth-category": "पन्ना जहाँ अनस्ट्रिप गहिराई सीमा पार क चुकल बाटे",
+       "unstrip-size-warning": "अनस्ट्रिप साइज के सीमा पार हो चुकल बा ($1)",
+       "unstrip-size-category": "पन्ना जहाँ अनस्ट्रिप साइज के सीमा पार हो चुकल बाटे",
        "converter-manual-rule-error": "मैनुअल भाषा परिवर्तन नियम में खराबी पकड़ल गइल",
        "undo-success": "संपादन वापस कइल जा सकत बा।\nनीचे दिहल तुलना के चेक करीं आ पुष्टी करीं की आप इहे कइल चाहत बाड़ीं, ओकरा बाद बदलाव सहेज के संपादन वापसी के पूरा करीं।",
        "undo-failure": "बीच में अउरी संपादन होखला की कारण ई संपादन वापस नइखे लिहल जा सकत।",
        "revdelete-modify-missing": "आइटम ID $1 के बदलाव करे में खराबी: ई डेटाबेस से गायब बा!",
        "revdelete-no-change": "<strong>चेतावनी:</strong> तारीख $2 के $1 बजे के ई आइटम पहिलहीं से ओही देखावे के सेटिंग वाला बाटे जवन माँगल जाता।",
        "revdelete-concurrent-change": "$1 $2 बजे के आइटम के बदले में खराबी आ रहल बा: बुझाता कि आप के कोसिस करे दौरान एकरा स्थिति के दूसर केहू बदल चुकल बा।\nलॉग चेक करीं।",
+       "revdelete-only-restricted": "तारीख $2, $1 के आइटम के लुकवावे में खराबी आवत बा: आप प्रबंधक लोग के नजर में आवे से आइटम के ना दबा सकत बाड़ीं जबले कि कौनों अउरी विजिबिलिटी बिकल्प के भी न सेलेक्ट करीं।",
+       "revdelete-reason-dropdown": "*हटावे के आम कारण\n** कॉपीराइट उलंघन\n** अनुचित कमेंट भा पर्सनल जानकारी\n** अनुचित प्रयोगकर्तानाँव\n** मानहानि-कारक जानकारी",
        "revdelete-otherreason": "अन्य/अतिरिक्त कारण:",
        "revdelete-reasonotherlist": "अन्य कारण",
        "revdelete-edit-reasonlist": "हटावे के कारण बदलीं",
index 897d11b..c12d9bd 100644 (file)
        "rcfilters-clear-all-filters": "Ерриге литтарш цӀанъян",
        "rcfilters-show-new-changes": "ТӀеххьара хийцамаш",
        "rcfilters-search-placeholder": "Литтаран керла хийцамаш лахар",
+       "rcfilters-empty-filter": "Жигара литтарш дац. Дерриге нисдарш гойтуш ю.",
        "rcfilters-filterlist-title": "Литтарш",
        "rcfilters-filterlist-feedbacklink": "Керла (бета) литтарех лаьцна хьайна хеттарг язде",
        "rcfilters-highlightbutton-title": "Билгалде карийнарш",
index 741738a..09b2d3e 100644 (file)
        "recentchangeslinked-feed": "Related changes",
        "recentchangeslinked-toolbox": "Related changes",
        "recentchangeslinked-title": "Changes related to \"$1\"",
-       "recentchangeslinked-summary": "Enter a page name to see changes on pages linked to or from that page. (To see members of a category, enter Category:Name of category). Changes to pages on [[Special:Watchlist|your Watchlist]] are in <strong>bold</strong>.",
+       "recentchangeslinked-summary": "Enter a page name to see changes on pages linked to or from that page. (To see members of a category, enter {{ns:category}}:Name of category). Changes to pages on [[Special:Watchlist|your Watchlist]] are in <strong>bold</strong>.",
        "recentchangeslinked-page": "Page name:",
        "recentchangeslinked-to": "Show changes to pages linked to the given page instead",
        "recentchanges-page-added-to-category": "[[:$1]] added to category",
index eb55654..18fd37e 100644 (file)
        "changepassword-throttled": "Has intentado acceder demasiadas veces recientemente.\nEspera $1 antes de intentarlo de nuevo.",
        "botpasswords": "Contraseñas de bots",
        "botpasswords-summary": "Las <em>contraseñas de bots</em> permiten el acceso a una cuenta de usuario mediante la API sin usar las credenciales principales de la cuenta. Los derechos de un usuario mientras haya iniciado sesión con una contraseña de bot pueden estar restringidos.\n\nSi no sabes por qué querrías hacer esto, probablemente no deberías hacerlo. Nadie debería pedirte que generes una de estas claves y que se la entregues.",
-       "botpasswords-disabled": "Las contraseñas de bot están desactivadas.",
+       "botpasswords-disabled": "Las contraseñas de robot están desactivadas.",
        "botpasswords-no-central-id": "Para usar una contraseña de bot, debes estar conectado a una cuenta centralizada.",
        "botpasswords-existing": "Contraseñas de bots existentes",
-       "botpasswords-createnew": "Crear una nueva contraseña de bot",
-       "botpasswords-editexisting": "Editar una contraseña de bot existente",
-       "botpasswords-label-appid": "Nombre del bot:",
+       "botpasswords-createnew": "Crear una contraseña de robot nueva",
+       "botpasswords-editexisting": "Editar una contraseña de robot existente",
+       "botpasswords-label-appid": "Nombre del robot:",
        "botpasswords-label-create": "Crear",
        "botpasswords-label-update": "Actualizar",
        "botpasswords-label-cancel": "Cancelar",
        "botpasswords-label-grants": "Permisos aplicables:",
        "botpasswords-help-grants": "Cada concesión le da acceso a los permisos listados que el usuario ya posea. Habilitar una concesión aquí no proporciona acceso a ningún permiso que tu cuenta de usuario no tendría de otra manera. Véase la [[Special:ListGrants|lista de concesiones]] para más información.",
        "botpasswords-label-grants-column": "Concedido",
-       "botpasswords-bad-appid": "El nombre del bot \"$1\" no es válido.",
+       "botpasswords-bad-appid": "El nombre del robot «$1» no es válido.",
        "botpasswords-insert-failed": "No se pudo agregar el nombre del bot \"$1\". ¿Ya ha sido añadido?",
        "botpasswords-update-failed": "No se pudo actualizar el nombre del bot \"$1\". ¿Ha sido borrado?",
-       "botpasswords-created-title": "Se creó la contraseña de bot",
+       "botpasswords-created-title": "Se creó la contraseña de robot",
        "botpasswords-created-body": "Se creó la contraseña del robot «$1» perteneciente {{GENDER:$2|al usuario|a la usuaria}} «$2».",
-       "botpasswords-updated-title": "Se actualizó la contraseña de bot",
+       "botpasswords-updated-title": "Se actualizó la contraseña de robot",
        "botpasswords-updated-body": "Se actualizó la contraseña del robot «$1» perteneciente {{GENDER:$2|al usuario|a la usuaria}} «$2».",
-       "botpasswords-deleted-title": "Se eliminó la contraseña de bot",
+       "botpasswords-deleted-title": "Se eliminó la contraseña de robot",
        "botpasswords-deleted-body": "Se eliminó la contraseña del robot «$1» perteneciente {{GENDER:$2|al usuario|a la usuaria}} «$2».",
        "botpasswords-newpassword": "La contraseña nueva para acceder con <strong>$1</strong> es <strong>$2</strong>. <em>Guarda esta información para su consulta futura.</em> <br> (En caso de robots antiguos que requieren que el nombre de acceso coincida con el de usuario, también puedes utilizar <strong>$3</strong> como nombre de usuario y <strong>$4</strong> como contraseña.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider no está disponible.",
        "group": "Grupo:",
        "group-user": "Usuarios",
        "group-autoconfirmed": "Autoconfirmados",
-       "group-bot": "Bots",
+       "group-bot": "Robots",
        "group-sysop": "Administradores",
        "group-bureaucrat": "Burócratas",
        "group-suppress": "Supresores de Flow",
        "group-all": "(todos)",
        "group-user-member": "{{GENDER:$1|usuario|usuaria}}",
        "group-autoconfirmed-member": "{{GENDER:$1|autoconfirmado|autoconfirmada}}",
-       "group-bot-member": "{{GENDER:$1|bot}}",
+       "group-bot-member": "{{GENDER:$1|robot}}",
        "group-sysop-member": "{{GENDER:$1|administrador|administradora}}",
        "group-bureaucrat-member": "{{GENDER:$1|burócrata}}",
        "group-suppress-member": "{{GENDER:$1|supresor|supresora}} de Flow",
        "grouppage-user": "{{ns:project}}:Usuarios",
        "grouppage-autoconfirmed": "{{ns:project}}:Autoconfirmados",
-       "grouppage-bot": "{{ns:project}}:Bots",
+       "grouppage-bot": "{{ns:project}}:Robots",
        "grouppage-sysop": "{{ns:project}}:Administradores",
        "grouppage-bureaucrat": "{{ns:project}}:Burócratas",
        "grouppage-suppress": "{{ns:project}}:Supresores de Flow",
        "right-editmyprivateinfo": "Editar su propia información privada (ej.: correo electrónico, nombre real)",
        "right-editmyoptions": "Editar tus preferencias",
        "right-rollback": "Revertir rápidamente las ediciones del último usuario que modificó una página en particular",
-       "right-markbotedits": "Marcar ediciones revertidas como ediciones de bot",
+       "right-markbotedits": "Marcar las reversiones como ediciones de robot",
        "right-noratelimit": "No resultar afectado por los límites de frecuencia de edición",
        "right-import": "Importar páginas desde otras wikis",
        "right-importupload": "Importar páginas desde un archivo",
        "rcfilters-filter-user-experience-level-experienced-label": "Usuarios experimentados",
        "rcfilters-filter-user-experience-level-experienced-description": "Editores registrados con más de 500 ediciones y 30 días de actividad.",
        "rcfilters-filtergroup-automated": "Contribuciones automatizadas",
-       "rcfilters-filter-bots-label": "Bot",
+       "rcfilters-filter-bots-label": "Robot",
        "rcfilters-filter-bots-description": "Ediciones realizadas por herramientas automatizadas.",
-       "rcfilters-filter-humans-label": "Ser humano (no bot)",
+       "rcfilters-filter-humans-label": "Ser humano (no robot)",
        "rcfilters-filter-humans-description": "Ediciones realizadas por editores humanos.",
        "rcfilters-filtergroup-reviewstatus": "Estado de revisión",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "No patrulladas",
        "rcfilters-filter-reviewstatus-manual-label": "Verificado manualmente",
-       "rcfilters-filter-reviewstatus-auto-description": "Ediciones por usuarios avanzadus cuyo trabajo es marcado automáticamente como verificado.",
+       "rcfilters-filter-reviewstatus-auto-description": "Ediciones por usuarios avanzados cuyo trabajo se marca automáticamente como verificado.",
        "rcfilters-filter-reviewstatus-auto-label": "Autoverificado",
        "rcfilters-filtergroup-significance": "Significación",
        "rcfilters-filter-minor-label": "Ediciones menores",
        "import-upload-username-prefix": "Prefijo de interwiki:",
        "import-assign-known-users": "Asignar ediciones a usuarios locales cuando el usuario correspondiente exista localmente",
        "import-comment": "Comentario:",
-       "importtext": "Por favor, exporta el archivo desde el wiki de origen usando la [[Special:Export|herramienta de exportación]], guárdalo en tu disco y súbelo aquí.",
+       "importtext": "Exporta el archivo desde el wiki de origen mediante la [[Special:Export|herramienta de exportación]], guárdalo en tu disco y cárgalo aquí.",
        "importstart": "Importando páginas...",
        "import-revision-count": "$1 {{PLURAL:$1|revisión|revisiones}}",
        "importnopages": "No hay páginas que importar.",
        "noscript.css": "/* Los estilos CSS colocados aquí se aplicarán a los usuarios que hayan desactivado el JavaScript en su navegador */",
        "group-autoconfirmed.css": "/* Los estilos CSS colocados aquí se aplicarán para todos los usuarios del grupo Usuarios autoconfirmados */",
        "group-user.css": "/* Los estilos CSS colocados aquí se aplicarán para todos los usuarios registrados */",
-       "group-bot.css": "/* Los estilos CSS colocados aquí se aplicarán para todos los usuarios del grupo Bots */",
+       "group-bot.css": "/* Los estilos CSS colocados aquí se aplicarán para todos los usuarios del grupo Robots */",
        "group-sysop.css": "/* Los estilos CSS colocados aquí se aplicarán para todos los usuarios del grupo Administradores */",
        "group-bureaucrat.css": "/* Los estilos CSS colocados aquí se aplicarán para todos los usuarios del grupo Burócratas */",
        "common.js": "/* Cualquier código JavaScript escrito aquí se cargará para todos los usuarios en cada carga de página */",
        "group-autoconfirmed.js": "/* Cualquier código JavaScript escrito aquí se cargará para todos los usuarios del grupo Usuarios autoconfirmados */",
        "group-user.js": "/* Cualquier código JavaScript escrito aquí se cargará para todos los usuarios registrados */",
-       "group-bot.js": "/* Cualquier código JavaScript escrito aquí se cargará para todos los usuarios del grupo Bots */",
+       "group-bot.js": "/* Cualquier código JavaScript escrito aquí se cargará para todos los usuarios del grupo Robots */",
        "group-sysop.js": "/* Cualquier código JavaScript escrito aquí se cargará para todos los usuarios del grupo Administradores */",
        "group-bureaucrat.js": "/* Cualquier código JavaScript escrito aquí se cargará para todos los usuarios del grupo Burócratas */",
        "anonymous": "{{PLURAL:$1|Usuario anónimo|Usuarios anónimos}} de {{SITENAME}}",
index 5c39a17..c792ba4 100644 (file)
        "savechanges": "Aldaketak gorde",
        "publishpage": "Orrialdea argitaratu",
        "publishchanges": "Aldaketak argitaratu",
+       "savechanges-start": "Aldaketak gorde...",
+       "publishpage-start": "Orrialdea argitaratu...",
+       "publishchanges-start": "Aldaketak argitaratu...",
        "preview": "Aurrebista erakutsi",
        "showpreview": "Aurrebista erakutsi",
        "showdiff": "Aldaketak erakutsi",
        "prefs-dateformat": "Data-formatua",
        "prefs-timeoffset": "Denbora ezberdintasuna",
        "prefs-advancedediting": "Genero aukerak",
+       "prefs-developertools": "Garatzaile tresnak",
        "prefs-editor": "Editorea",
        "prefs-preview": "Aurreikusi",
        "prefs-advancedrc": "Aukera aurreratuak",
        "rcfilters-filter-humans-description": "Gizaki editoreek egindako aldaketak.",
        "rcfilters-filtergroup-reviewstatus": "Berrikuspenaren egoera",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "Patruilagabea",
+       "rcfilters-filter-reviewstatus-manual-label": "Eskuz patruilatuak",
        "rcfilters-filtergroup-significance": "Munta",
        "rcfilters-filter-minor-label": "Aldaketa txikiak",
        "rcfilters-filter-minor-description": "Egileak sailkatutako aldaketa txikiak.",
        "deadendpages": "Orrialde itsuak",
        "deadendpagestext": "Jarraian zerrendatutako orrialdeek ez daukate wikiko beste edozein orrialdetarako loturarik.",
        "protectedpages": "Babestutako orrialdeak",
+       "protectedpages-filters": "Iragazkiak:",
        "protectedpages-indef": "Babes mugagabeak bakarrik",
        "protectedpages-summary": "Orrialde honetan unean babestutako orriak zerrendatzen dira. Sorkuntza babesten duten izenen zerrenda lortzeko, ikusi [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
        "protectedpages-cascade": "Kaskada moduko babesak bakarrik",
        "apisandbox-dynamic-error-exists": "$1 parametro izena dagoeneko existitzen da",
        "apisandbox-deprecated-parameters": "Aurretiaz zehaztutako parametroak",
        "apisandbox-fetch-token": "Token-a automatikoki bete",
+       "apisandbox-add-multi": "Gehitu",
        "apisandbox-submit-invalid-fields-title": "Zelai batzuk ez dute balio.",
        "apisandbox-submit-invalid-fields-message": "Mesedez, zuzendu markatutako zelaiak eta saiatu berrio.",
        "apisandbox-results": "Emaitzak",
        "version-specialpages": "Aparteko orrialdeak",
        "version-parserhooks": "Parser estentsioak",
        "version-variables": "Aldagaiak",
+       "version-editors": "Editoreak",
        "version-antispam": "Spam ekiditea",
        "version-other": "Bestelakoak",
        "version-mediahandlers": "Media gordailuak",
index 4d3f927..1db40fd 100644 (file)
        "rcfilters-filter-humans-label": "Ihminen (ei botti)",
        "rcfilters-filter-humans-description": "Ihmisten tekemät muokkaukset.",
        "rcfilters-filtergroup-reviewstatus": "Sivun partioinnin status",
+       "rcfilters-filter-reviewstatus-unpatrolled-description": "Muutoksia ei ole merkitty manuaalisesti tai automaattisesti partioiduksi.",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "Ei ole partioitu",
+       "rcfilters-filter-reviewstatus-manual-description": "Muutokset merkitty partioiduksi manuaalisesti.",
+       "rcfilters-filter-reviewstatus-manual-label": "Manuaalisesti partioitu",
+       "rcfilters-filter-reviewstatus-auto-description": "Muutokset edistyneiltä käyttäjiltä joiden työ on automaattisesti merkitty partioiduksi.",
+       "rcfilters-filter-reviewstatus-auto-label": "Automaattisesti partioitu",
        "rcfilters-filtergroup-significance": "Merkitys",
        "rcfilters-filter-minor-label": "Pienet muutokset",
        "rcfilters-filter-minor-description": "Muokkaukset, jotka on merkitty pieniksi.",
        "rcfilters-filter-watchlistactivity-seen-label": "Nähdyt muutokset",
        "rcfilters-filter-watchlistactivity-seen-description": "Muutokset sivuihin, joilla olet käynyt muutosten jälkeen.",
        "rcfilters-filtergroup-changetype": "Muutoksen tyyppi",
-       "rcfilters-filter-pageedits-label": "Sivun muokkaukset",
+       "rcfilters-filter-pageedits-label": "Sivun muutokset",
        "rcfilters-filter-pageedits-description": "Muokkaukset wikin sisältöön, keskusteluihin, luokkakuvauksiin…",
        "rcfilters-filter-newpages-label": "Sivujen luonnit",
        "rcfilters-filter-newpages-description": "Muokkaukset, joilla on luotu uusia sivuja.",
index 918e4bd..1c372e5 100644 (file)
        "customcssprotected": "Non ten os permisos necesarios para modificar esta páxina de CSS, dado que contén a configuración persoal doutro usuario.",
        "customjsprotected": "Non ten os permisos necesarios para modificar esta páxina de JavaScript, dado que contén a configuración persoal doutro usuario.",
        "mycustomcssprotected": "Non ten os permisos necesarios para editar esta páxina de CSS.",
+       "mycustomjsonprotected": "Non ten permisos para editar esta páxina JSON.",
        "mycustomjsprotected": "Non ten os permisos necesarios para editar esta páxina de JavaScript.",
        "myprivateinfoprotected": "Non ten os permisos necesarios para editar a súa información privada.",
        "mypreferencesprotected": "Non ten os permisos necesarios para editar as súas preferencias.",
        "wrongpasswordempty": "O campo do contrasinal estaba en branco.\nPor favor, inténteo de novo.",
        "passwordtooshort": "Os contrasinais deben conter, como mínimo, {{PLURAL:$1|1 carácter|$1 caracteres}}.",
        "passwordtoolong": "Os contrasinais non poden ser máis longo de {{PLURAL:$1|1 carácter|$1 caracteres}}.",
-       "passwordtoopopular": "Non pode utilizar un contrasinal dos habitualmente elixidos pola xente. Por favor, escolla un contrasinal máis orixinal.",
+       "passwordtoopopular": "Non pode utilizar un contrasinal dos habitualmente elixidos pola xente. Por favor, escolla un contrasinal que sexa máis complicada de adiviñar.",
        "password-name-match": "O seu contrasinal debe ser diferente do seu nome de usuario.",
        "password-login-forbidden": "O uso deste nome de usuario e contrasinal foi prohibido.",
        "mailmypassword": "Restablecer o contrasinal",
        "passwordremindertitle": "Novo contrasinal temporal para {{SITENAME}}",
-       "passwordremindertext": "Alguén (probablemente vostede, desde o enderezo IP $1) solicitou un novo\ncontrasinal para acceder a {{SITENAME}} ($4). Creouse un contrasinal temporal para o usuario\n\"$2\" e quedou establecido como \"$3\". Se esa foi a súa\nintención, terá que acceder ao sistema e escoller un novo contrasinal agora.\nO seu contrasinal temporal caducará {{PLURAL:$5|nun día|en $5 días}}.\n\nSe foi outra persoa a que fixo esta solicitude ou se xa se lembra do seu contrasinal\ne non o quere modificar, pode ignorar esta mensaxe e\ncontinuar a utilizar o seu contrasinal vello.",
+       "passwordremindertext": "Alguén (desde o enderezo IP $1) solicitou un novo\ncontrasinal para acceder a {{SITENAME}} ($4). Creouse un contrasinal temporal para o usuario\n\"$2\" e quedou establecido como \"$3\". Se esa foi a súa\nintención, terá que acceder ao sistema e escoller un novo contrasinal agora.\nO seu contrasinal temporal caducará {{PLURAL:$5|nun día|en $5 días}}.\n\nSe foi outra persoa a que fixo esta solicitude ou se xa se lembra do seu contrasinal\ne non o quere modificar, pode ignorar esta mensaxe e\ncontinuar a utilizar o seu contrasinal vello.",
        "noemail": "O usuario \"$1\" non posúe ningún enderezo de correo electrónico rexistrado.",
        "noemailcreate": "Ten que proporcionar un enderezo de correo electrónico válido",
        "passwordsent": "Enviouse un contrasinal novo ao enderezo de correo electrónico rexistrado de \"$1\".\nPor favor, acceda ao sistema de novo tras recibilo.",
        "sitecsspreview": "'''Lembre que só está vendo a vista previa deste CSS.'''\n'''Este aínda non foi gardado!'''",
        "sitejsonpreview": "<strong>Lembre que tan só está previsualizando esta configuración JSON.\nAínda non foi gardada!</strong>",
        "sitejspreview": "'''Lembre que só está vendo a vista previa deste código JavaScript.'''\n'''Este aínda non foi gardado!'''",
-       "userinvalidconfigtitle": "<strong>Aviso:</strong> Non hai ningunha aparencia chamada \"$1\".\nLembre que as páxinas .css e .js personalizadas utilizan un título en minúsculas, como por exemplo \"{{ns:user}}:Exemplo/vector.css\" no canto de \"{{ns:user}}:Exemplo/Vector.css\".",
+       "userinvalidconfigtitle": "<strong>Aviso:</strong> Non hai ningunha aparencia chamada \"$1\".\nLembre que as páxinas .css, .json e .js personalizadas utilizan un título en minúsculas, como por exemplo \"{{ns:user}}:Exemplo/vector.css\" no canto de \"{{ns:user}}:Exemplo/Vector.css\".",
        "updated": "(Actualizado)",
        "note": "'''Nota:'''",
        "previewnote": "<strong>Lembre que esta é só unha vista previa.</strong>\nAínda non gardou os seus cambios!",
        "longpageerror": "'''Erro: O texto que pretende gardar ocupa {{PLURAL:$1|$1 kilobyte|$1 kilobytes}}, e existe un límite dun máximo de {{PLURAL:$2|$2 kilobyte|$2 kilobytes}}.'''\nPolo tanto, non se pode gardar.",
        "readonlywarning": "<strong>Atención: Pechouse a base de datos para facer mantemento, polo que non vai poder gardar as súas edicións polo de agora.</strong>\nSe cadra, pode cortar e pegar o texto nun ficheiro de texto e gardalo para despois.\n\nO administrador do sistema que a pechou deu esta explicación: $1",
        "protectedpagewarning": "'''Aviso: Esta páxina foi protexida de xeito que só os usuarios con privilexios de administrador a poidan editar.'''\nVelaquí está a última entrada no rexistro, por se quere consultala:",
-       "semiprotectedpagewarning": "'''Nota:''' Esta páxina foi protexida de xeito que só os usuarios rexistrados a poidan editar.\nVelaquí está a última entrada no rexistro, por se quere consultala:",
+       "semiprotectedpagewarning": "<strong>Nota:</strong> Esta páxina foi protexida de xeito que só os usuarios autoconfirmados a poidan editar.\nVelaquí está a última entrada no rexistro, por se quere consultala:",
        "cascadeprotectedwarning": "<strong>Atención:</strong> Protexeuse esta páxina de xeito que só a poden editar os usuarios con [[Special:ListGroupRights|privilexios específicos]] debido a que está transcluída {{PLURAL:$1|na seguinte páxina protexida|nas seguintes páxinas protexidas}} coa opción \"protección en serie\" activada:",
        "titleprotectedwarning": "'''Aviso: Esta páxina foi protexida de xeito que [[Special:ListGroupRights|só algúns usuarios]] a poidan crear.'''\nVelaquí está a última entrada no rexistro, por se quere consultala:",
        "templatesused": "{{PLURAL:$1|Modelo usado|Modelos usados}} nesta páxina:",
        "prefs-dateformat": "Formato da data",
        "prefs-timeoffset": "Desprazamento horario",
        "prefs-advancedediting": "Opcións xerais",
+       "prefs-developertools": "Ferramentas de desenvolvemento",
        "prefs-editor": "Editor",
        "prefs-preview": "Vista previa",
        "prefs-advancedrc": "Opcións avanzadas",
        "rcfilters-filter-humans-description": "Edicións realizadas por editores humanos.",
        "rcfilters-filtergroup-reviewstatus": "Estado de revisión",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "Sen patrullar",
+       "rcfilters-filter-reviewstatus-manual-description": "Edicións marcadas manualmente como vixiadas.",
+       "rcfilters-filter-reviewstatus-manual-label": "Vixiadas manualmente",
+       "rcfilters-filter-reviewstatus-auto-description": "Edicións realizadas por usuarios avanzados cuxo traballo márcase automaticamente como vixiado.",
+       "rcfilters-filter-reviewstatus-auto-label": "Vixiado automaticamente",
        "rcfilters-filtergroup-significance": "Importancia",
        "rcfilters-filter-minor-label": "Edicións menores",
        "rcfilters-filter-minor-description": "Edicións que o autor etiquetou como menores.",
        "rollback-success": "Desfixéronse as edicións de {{GENDER:$3|$1}};\nvolveuse á última edición, feita por {{GENDER:$4|$2}}.",
        "rollback-success-notify": "Revertéronse as edicións de $1;\nrestaurouse a última revisión de $2. [$3 Mostrar os cambios]",
        "sessionfailure-title": "Erro de sesión",
-       "sessionfailure": "Parece que hai un problema co rexistro da súa sesión;\nesta acción cancelouse como precaución fronte ao secuestro de sesións.\nPrema no botón \"atrás\", volva cargar a páxina da que proviña e inténteo de novo.",
+       "sessionfailure": "Parece que hai un problema co rexistro da súa sesión;\nesta acción cancelouse como precaución fronte ao secuestro de sesións.\nPor favor, volva enviar o formulario.",
        "changecontentmodel": "Cambiar o modelo de contido dunha páxina",
        "changecontentmodel-legend": "Cambiar o modelo de contido",
        "changecontentmodel-title-label": "Título da páxina",
        "fix-double-redirects": "Actualizar calquera redirección que apunte cara ao título orixinal",
        "move-leave-redirect": "Deixar unha redirección detrás",
        "protectedpagemovewarning": "'''Aviso:''' Esta páxina foi protexida de xeito que só os usuarios con privilexios de administrador a poidan mover.\nVelaquí está a última entrada no rexistro, por se quere consultala:",
-       "semiprotectedpagemovewarning": "'''Nota:''' Esta páxina foi protexida de xeito que só os usuarios rexistrados a poidan mover.\nVelaquí está a última entrada no rexistro, por se quere consultala:",
+       "semiprotectedpagemovewarning": "<strong>Nota:</strong> Esta páxina foi protexida de xeito que só os usuarios autoconfirmados a poidan mover.\nVelaquí está a última entrada no rexistro, por se quere consultala:",
        "move-over-sharedrepo": "\"[[:$1]]\" xa existe nun repositorio compartido. Ao mover un ficheiro a este título sobrescribirase o ficheiro compartido.",
        "file-exists-sharedrepo": "O nome que elixiu para o ficheiro xa está en uso nun repositorio compartido.\nPor favor, escolla outro nome.",
        "export": "Exportar páxinas",
        "unlinkaccounts-success": "A conta foi desvinculada.",
        "authenticationdatachange-ignored": "Os cambios de datos de autenticación non foron xerados. Está configurado o provedor?",
        "userjsispublic": "Lembre: As subpáxinas JavaScript non deberían conter datos confidenciais porque outros usuarios poden velos.",
+       "userjsonispublic": "Por favor, teña en conta queː as subpáxinas JSON non deben conter datos confidenciais xa que son visibles por outros usuarios.",
        "usercssispublic": "Lembre: As subpáxinas CSS non deberían conter datos confidenciais porque outros usuarios poden velos.",
        "restrictionsfield-badip": "Enderezo IP ou rango de IP non válido: $1",
        "restrictionsfield-label": "Rangos de IP permitidos:",
index 0fb50d1..c20415e 100644 (file)
        "uploaddisabled": "העלאת קבצים מבוטלת.",
        "copyuploaddisabled": "העלאת קבצים מכתובת URL מבוטלת.",
        "uploaddisabledtext": "אפשרות העלאת הקבצים מבוטלת.",
-       "php-uploaddisabledtext": "אפשרות העלאת הקבצים מבוטלת ברמת PHP. אנא בדקו את ההגדרה file_uploads.",
+       "php-uploaddisabledtext": "אפשרות העלאת הקבצים מבוטלת ברמת PHP.\nנא לבדוק את ההגדרה file_uploads.",
        "uploadscripted": "הקובץ כולל קוד סקריפט או HTML שעשוי להתפרש או להתבצע בטעות על־ידי הדפדפן.",
        "upload-scripted-pi-callback": "לא ניתן להעלות קובץ שמכיל את הוראת העיבוד XML-stylesheet.",
-       "upload-scripted-dtd": "לא ניתן להעלות קבצי SVG שכוללים הכרזת DTD לא־סטנדרטית.",
+       "upload-scripted-dtd": "×\9c×\90 × ×\99ת×\9f ×\9c×\94×¢×\9c×\95ת ×§×\95×\91צ×\99 SVG ×©×\9b×\95×\9c×\9c×\99×\9d ×\94×\9bר×\96ת DTD ×\9c×\90־ס×\98× ×\93ר×\98×\99ת.",
        "uploaded-script-svg": "נמצא אלמנט שאפשר לכתוב בו תסריט \"$1\" בקובץ ה־SVG שהועלה.",
        "uploaded-hostile-svg": "נמצא CSS בלתי־מאובטח באלמנט style בקובץ ה־SVG שהועלה.",
        "uploaded-event-handler-on-svg": "אסור להגדיר מאפייני טיפול באירועים <code dir=\"ltr\">$1=\"$2\"</code> בקובצי SVG.",
        "uploaded-href-attribute-svg": "רכיבי <a> יכולים לקשר (href) רק ליעדי data:‎ (קובץ מוטמע), http://‎ או https://‎, או מקטע (עם #, באותו מסמך). ברכיבים אחרים, כגון <image>, מותרים רק יעדי data:‎ ומקטע. באפשרותך לנסות להטמיע תמונות בעת ייצוא קובץ ה־SVG שלך. נמצא <code dir=\"ltr\">&lt;$1 $2=\"$3\"&gt;</code>.",
-       "uploaded-href-unsafe-target-svg": "נמצא href לנתונים לא מאובטחים <code dir=\"ltr\">&lt;$1 $2=\"$3\"&gt;</code> בקובץ ה־SVG שהועלה.",
+       "uploaded-href-unsafe-target-svg": "נמצא href לנתונים לא מאובטחים: יעד URI <code dir=\"ltr\">&lt;$1 $2=\"$3\"&gt;</code> בקובץ ה־SVG שהועלה.",
        "uploaded-animate-svg": "נמצא תג \"animate\" שיכול לשנות href באמצעות מאפיין \"from\"  בצורת <code dir=\"ltr\">&lt;$1 $2=\"$3\"&gt;</code> בקובץ ה־SVG שהועלה.",
        "uploaded-setting-event-handler-svg": "הגדרת מאפייני טיפול באירועים חסומה, נמצא <code dir=\"ltr\">&lt;$1 $2=\"$3\"&gt;</code> בקובץ ה־SVG שהועלה.",
-       "uploaded-setting-href-svg": "השימוש בתג set כדי להוסיף מאפיין href לאלמנט הורה חסום.",
+       "uploaded-setting-href-svg": "השימוש בתג \"set\" כדי להוסיף מאפיין \"href\" לאלמנט הורה חסום.",
        "uploaded-wrong-setting-svg": "השימוש בתג \"set\" כדי להוסיף יעד remote/data/script לכל מאפיין חסום. נמצא <code dir=\"ltr\">&lt;set to=\"$1\"&gt;</code> בקובץ ה־SVG שהועלה.",
        "uploaded-setting-handler-svg": "SVG שמגדיר את המאפיין \"handler\" עם remote/data/script חסום. נמצא <code dir=\"ltr\">$1=\"$2\"</code> בקובץ ה־SVG שהועלה.",
        "uploaded-remote-url-svg": "SVG שמגדיר כל מאפיין style עם URL מרוחק חסום. נמצא <code dir=\"ltr\">$1=\"$2\"</code> בקובץ ה־SVG שהועלה.",
        "uploaded-image-filter-svg": "נמצא מסנן תמונה עם URL‏: <code dir=\"ltr\">&lt;$1 $2=\"$3\"&gt;</code> בקובץ ה־SVG שהועלה.",
-       "uploadscriptednamespace": "ק×\95×\91×¥ ×\94â\80\8fâ\80\8fÖ«Ö¾SVG ×\94×\96×\94 ×\9b×\95×\9c×\9c ×\9eר×\97×\91 ×©×\9d ×\91×\9cת×\99 חוקי \"<nowiki>$1</nowiki>\".",
+       "uploadscriptednamespace": "ק×\95×\91×¥ ×\94â\80\8fâ\80\8fÖ¾SVG ×\94×\96×\94 ×\9b×\95×\9c×\9c ×\9eר×\97×\91 ×©×\9d ×\91×\9cת×\99Ö¾חוקי \"<nowiki>$1</nowiki>\".",
        "uploadinvalidxml": "לא ניתן לפרש את ה־XML בקובץ שהועלה.",
        "uploadvirus": "הקובץ מכיל וירוס!\nפרטים:\n<div dir=\"ltr\">$1</div>",
        "uploadjava": "קובץ זה הוא קובץ ZIP שמכיל קובץ &lrm;.class של Java.\nהעלאת קובצי Java אסורה, כיוון שהם יכולים לגרום לעקיפת מגבלות האבטחה.",
        "upload-description": "תיאור הקובץ",
        "upload-options": "אפשרויות העלאה",
        "watchthisupload": "מעקב אחרי קובץ זה",
-       "filewasdeleted": "ק×\95×\91×¥ ×\91ש×\9d ×\96×\94 ×\9b×\91ר ×\94×\95×¢×\9c×\94 ×\91×¢×\91ר, ×\95×\9c×\90×\97ר ×\9e×\9b×\9f × ×\9e×\97ק.\n×\90× ×\90 ×\91Ö´Ö¼×\93ק×\95 ×\90ת $1 ×\9cפנ×\99 ×©×ª×\9eש×\99×\9b×\95 ×\9c×\94×¢×\9c×\95ת את הקובץ שנית.",
+       "filewasdeleted": "ק×\95×\91×¥ ×\91ש×\9d ×\96×\94 ×\9b×\91ר ×\94×\95×¢×\9c×\94 ×\91×¢×\91ר, ×\95×\9c×\90×\97ר ×\9e×\9b×\9f × ×\9e×\97ק.\n×\99ש ×\9c×\91×\93×\95ק ×\90ת $1 ×\9cפנ×\99 ×\94×¢×\9cאת הקובץ שנית.",
        "filename-thumb-name": "נראה שכותרת הקובץ היא כותרת של תמונה מוקטנת (ממוזערת). יש להימנע מהעלאת תמונות ממוזערות בחזרה לאותו אתר ויקי. אם זו אינה תמונה ממוזערת, יש לתקן את שם הקובץ כך שיהיה משמעותי יותר ושלא יכלול את הקידומת של תמונה ממוזערת.",
-       "filename-bad-prefix": "ש×\9d ×\94ק×\95×\91×¥ ×©×\90ת×\9d ×\9e×¢×\9c×\99×\9d ×\9eת×\97×\99×\9c ×\91Ö¾<strong>\"$1\"</strong>, ×©×\94×\95×\90 ×©×\9d ×©×\90×\99× ×\95 ×\9eת×\90ר ×\90ת ×\94ק×\95×\91×¥ ×\95×\91×\93ר×\9a כלל מוקצה אוטומטית על־ידי מצלמות דיגיטליות.\nיש לבחור שם מתאים יותר לקובץ, שיתאר את תכניו.",
+       "filename-bad-prefix": "ש×\9d ×\94ק×\95×\91×¥ ×©×\91×\97רת ×\9c×\94×¢×\9c×\95ת ×\9eת×\97×\99×\9c ×\91Ö¾<strong>\"$1\"</strong>, ×©×\94×\95×\90 ×©×\9d ×©×\90×\99× ×\95 ×\9eת×\90ר ×\90ת ×\94ק×\95×\91×¥ ×\95×\91×\93ר×\9aÖ¾כלל מוקצה אוטומטית על־ידי מצלמות דיגיטליות.\nיש לבחור שם מתאים יותר לקובץ, שיתאר את תכניו.",
        "filename-prefix-blacklist": " #<!-- נא להשאיר שורה זו בדיוק כפי שהיא --> <pre>\n# התחביר הוא כדלקמן:\n#   * כל דבר מתו \"#\" לסוף השורה הוא הערה\n#   * כל שורה לא ריקה היא קידומת לשמות קבצים טיפוסיים שמצלמות דיגיטליות נותנות אוטומטית\nCIMG # Casio\nDSC_ # Nikon\nDSCF # Fuji\nDSCN # Nikon\nDUW # מספר טלפונים סלולריים\nIMG # כללי\nJD # Jenoptik\nMGP # Pentax\nPICT # שונות\n #</pre> <!-- נא להשאיר שורה זו בדיוק כפי שהיא -->",
        "upload-proto-error": "פרוטוקול שגוי",
-       "upload-proto-error-text": "בהעלאה מרוחקת, יש להשתמש בכתובות URL המתחילות עם <code>http://</code> או <code>ftp://</code>.",
+       "upload-proto-error-text": "בהעלאה מרוחקת, יש להשתמש בכתובות URL המתחילות עם <code dir=\"ltr\">http://</code> או עם <code dir=\"ltr\">ftp://</code>.",
        "upload-file-error": "שגיאה פנימית",
        "upload-file-error-text": "שגיאה פנימית התרחשה בעת הניסיון ליצור קובץ זמני על השרת.\nאנא צרו קשר עם [[Special:ListUsers/sysop|מפעיל מערכת]].",
        "upload-misc-error": "שגיאת העלאה בלתי ידועה",
index f5cda9d..fbc3d57 100644 (file)
        "savechanges": "Sačuvaj stranicu",
        "publishpage": "Objavi stranicu",
        "publishchanges": "Sačuvaj uređivanje",
+       "savearticle-start": "Sačuvaj stranicu...",
+       "savechanges-start": "Spremi promjene...",
+       "publishpage-start": "Objavi stranicu...",
        "publishchanges-start": "Sačuvaj uređivanje...",
        "preview": "Pregled kako će stranica izgledati",
        "showpreview": "Prikaži kako će izgledati",
        "accmailtext": "Nova zaporka za [[User talk:$1|$1]] je poslana na $2.\n\nNakon prijave, zaporka za ovaj novi račun može biti promijenjena na stranici ''[[Special:ChangePassword|promijeni zaporku]]'' nakon prijave.",
        "newarticle": "(Novo)",
        "newarticletext": "Došli ste na stranicu koja još ne postoji.\nAko želite stvoriti tu stranicu, počnite tipkati u prozor ispod ovog teksta (pogledajte [$1 stranicu za pomoć]).\nAko ste ovamo dospjeli slučajno, kliknite gumb '''natrag''' (back) u svom pregledniku.",
-       "anontalkpagetext": "----''Ovo je stranica za razgovor s neprijavljenim suradnikom koji još nije otvorio suradnički račun ili se njime ne koristi. Zbog toga se moramo služiti brojčanom IP adresom kako bismo ga identificirali. Takvu adresu često može dijeliti više ljudi. Ako ste neprijavljeni suradnik i smatrate da su Vam upućeni irelevantni komentari, molimo Vas da [[Special:CreateAccount|otvorite suradnički račun]] ili [[Special:UserLogin|se prijavite]] te tako u budućnosti izbjegnete zamjenu s drugim neprijavljenim suradnicima.''",
+       "anontalkpagetext": "----\n<em>Ovo je stranica za razgovor s neprijavljenim suradnikom koji još nije otvorio suradnički račun ili se njime ne koristi.</em>\nZbog toga se moramo služiti brojčanom IP adresom kako bismo ga identificirali. \nTakvu adresu često može dijeliti više ljudi. \nAko ste neprijavljeni suradnik i smatrate da su Vam upućeni irelevantni komentari, molimo Vas da [[Special:CreateAccount|otvorite suradnički račun]] ili [[Special:UserLogin|se prijavite]] te tako u budućnosti izbjegnete zamjenu s drugim neprijavljenim suradnicima.",
        "noarticletext": "Na ovoj stranici trenutačno nema sadržaja.\nMožete [[Special:Search/{{PAGENAME}}|potražiti ovaj naslov]] na drugim stranicama,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane evidencije]\nili [{{fullurl:{{FULLPAGENAME}}|action=edit}} stvoriti ovu stranicu]</span>.",
        "noarticletext-nopermission": "Ova stranica nema sadržaja.\nMožete [[Special:Search/{{PAGENAME}}|tražiti naslov ove stranice]] na drugim stranicama ili <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane evidencije]</span>, ali ne možete stvoriti ovu stranicu.",
        "missing-revision": "Uređivanje broj $1 na stranici \"{{FULLPAGENAME}}\" ne postoji.\n\nOvo je obično uzrokovano kada kliknete na zastarjelu poveznicu na stranice koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} evidenciji brisanja].",
index 8c6e304..8032223 100644 (file)
        "preview": "Previdar",
        "showpreview": "Previdar",
        "showdiff": "Montrez chanji",
-       "blankarticle": "<strong>Averto:</strong> La pagino vu kreas es vakua.\nSe vu ri-selektos \"$1\", la pagino kreesos sen irga kontenajo.",
+       "blankarticle": "<strong>Averto:</strong> La pagino quon vu kreis es vakua.\nSe vu ri-selektos \"$1\", la pagino kreesos sen irga kontenajo.",
        "anoneditwarning": "<strong>Averto:</strong> Vu ne eniris.\nVua IP-adreso esos videbla publike se vu redaktos. Se vu <strong>[$1 enirus]</strong> od <strong>[$2 kreus konto]</strong>, vua redakti atribuesos a vua uzeronomo, kune kun altra bonaji.",
        "anonpreviewwarning": "<em>Vu ne eniris. Konservar chanji registragos vua IP-adreso en la redakto-historio di ta pagino.</em>",
        "missingcommenttext": "Voluntez skribar komento.",
        "filesource": "Fonto:",
        "ignorewarning": "Ignorar la averto e gardar la arkivo irgakaze.",
        "badfilename": "La imajo-nomo chanjesis a \"$1\".",
+       "empty-file": "L'arkivo sendita da vu esas vakua.",
        "fileexists": "Arkivo kun ta nomo ja existas.\nVolutez kontrolar <strong>[[:$1]]</strong> se {{GENDER:|vu}} ne esas certa pri chanjar olu.\n[[$1|thumb]]",
        "filepageexists": "La pagino kun deskripto pri ica arkivo ja kreesis en <strong>[[:$1]]</strong>, tamen nul arkivo kun ica nomo existas ankore.\nLa rezumo pri ol quon vu skriptis ne aparos en la deskripto-pagino.\nPor ke la rezumo aparos ibe, vu mustos <strong>skribor ol manuale.</strong>\n[[$1|thumb]]",
        "uploadwarning": "Averto pri la adkargo di arkivo",
        "withoutinterwiki": "Pagini sen linguo-ligili",
        "withoutinterwiki-legend": "Prefixo",
        "withoutinterwiki-submit": "Montrar",
+       "fewestrevisions": "Pagini kun poka revizi",
        "nbytes": "$1 {{PLURAL:$1|bicoko|bicoki}}",
        "ncategories": "$1 {{PLURAL:$1|kategorio|kategorii}}",
        "nlinks": "$1 {{PLURAL:$1|ligilo|ligili}}",
        "booksources-search-legend": "Serchez librala fonti",
        "booksources-search": "Serchar",
        "booksources-text": "Infre vu povas vidar listo di ligili ad altra retsitui qui vendas nova ed uzata libri, ed anke povas havar informi pri la libri quin vu serchabas:\nLa {{SITENAME}} ne mantenas komercala relati kun ta vendeyi mencionata, e la listo ne povas konsideresar rekomendo o vend-anunco.",
+       "magiclink-tracking-pmid-desc": "Ica pagino uzas magiala ligili PMID. Videz [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] pri quale migrar.",
        "magiclink-tracking-isbn": "Pagini qui uzas ligili ISBN",
        "specialloguserlabel": "Agero:",
        "speciallogtitlelabel": "Skopo (titulo od {{ns:user}}:uzernomo por uzero):",
        "listgrouprights": "Permisi dil grupo di uzeri",
        "listgrouprights-group": "Grupo",
        "listgrouprights-members": "(listo di membri)",
+       "trackingcategories": "Kategorii por kontrolo",
+       "trackingcategories-name": "Nomo di la mesajo",
+       "trackingcategories-desc": "Kriterii por inkluzar kategorii",
        "restricted-displaytitle-ignored-desc": "La pagino havas nekonocita titulo <code><nowiki>{{DISPLAYTITLE}}</nowiki></code>, pro ol ne esas equivalanta a la nuna titulo di ica pagino.",
        "mailnologin": "Ne sendar adreso",
        "mailnologintext": "Vu mustas [[Special:UserLogin|enirir]] e havar valida e-adreso en vua [[Special:Preferences|preferaji]] por sendar e-posto ad altra uzanti.",
index ac1eb75..5ef25bf 100644 (file)
        "listduplicatedfiles": "Lies mit bestenj mit duplikaote",
        "listduplicatedfiles-summary": "Dit is 'n lies mit bestenj wovan de litste versie e duplikaot is van de recènste versie van 'n anger bestandj. Allein weurt gerapporteerd euver lokaal bestenj.",
        "listduplicatedfiles-entry": "[[:File:$1|$1]] haet [[$3|{{PLURAL:$2|ei duplikaot|$2 duplikaote}}]].",
-       "unusedtemplates": "Óngerbroekde sjablone",
+       "unusedtemplates": "Óngebroekde sjablone",
        "unusedtemplatestext": "Deze pagina guf alle pagina's weer in de {{nas:template}}naamruumde die op gein inkele pagina gebroek waere. Vergaet neet de \"Links nao deze pagina\" te controlere veures dit sjabloon te wösse.",
        "unusedtemplateswlh": "anger links",
        "randompage": "Willekäörige pagina",
        "confirm-purge-title": "Vernuuj dees pagina",
        "confirm_purge_button": "ok",
        "confirm-purge-top": "Wils te de buffer vaan dees paas wisse?",
-       "confirm-purge-bottom": "t Opsjone van de cache zorg drveur det de lèste versie van n pagina wörd weergegaeve.",
+       "confirm-purge-bottom": "'t Opsjeune van de cache zorg d'rveur det de lèste versie van 'n pagina weurt getuind.",
        "confirm-watch-button": "Ok",
        "confirm-watch-top": "Dees pagina bie dien volglies zètte?",
        "confirm-unwatch-button": "Ok",
index 3d6f7a8..6831398 100644 (file)
        "rcfilters-liveupdates-button-title-off": "Rādīt jaunās izmaiņas, tiklīdz tās tiek veiktas",
        "rcfilters-watchlist-markseen-button": "Atzīmēt visas izmaiņas kā apskatītas",
        "rcfilters-watchlist-edit-watchlist-button": "Labot manu uzraugāmo lapu sarakstu",
-       "rcfilters-watchlist-showupdated": "Izmaiņas lapās, kuras nav apmeklētas kopš izmaiņu veikšanas ir <strong>trekninātā rakstā</strong>.",
+       "rcfilters-watchlist-showupdated": "Izmaiņas lapās, kuras nav apmeklētas kopš izmaiņu veikšanas, ir <strong>trekninātā rakstā</strong>.",
        "rcfilters-preference-label": "Paslēpt uzlaboto pēdējo izmaiņu versiju",
        "rcnotefrom": "Zemāk {{PLURAL:$5|redzamas izmaiņas|redzama izmaiņa|redzamas izmaiņas}} kopš <strong>$3, $4</strong> (parādītas ne vairāk kā <strong>$1</strong>).",
        "rclistfromreset": "Atiestatīt datuma izvēli",
index 12f0158..62ccc4f 100644 (file)
@@ -10,7 +10,8 @@
                        "VoteITP",
                        "아라",
                        "Macofe",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Baloch Khan"
                ]
        },
        "tog-underline": "Garih bawahi tautan:",
        "october-date": "$1 Oktober",
        "november-date": "$1 Nopember",
        "december-date": "$1 Desember",
+       "period-am": "سهار",
+       "period-pm": "ماښام",
        "pagecategories": "{{PLURAL:$1|Kategori}}",
        "category_header": "Laman pado kategori \"$1\"",
        "subcategories": "Subkategori",
        "nospecialpagetext": "<strong>Sanak mamintak laman istimewa nan indak sah.</strong>\n\nDaftar laman istimewa nan sah dapek dicaliak di [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Kasalahan",
        "databaseerror": "Kasalahan basis data",
+       "databaseerror-error": "تېروتنه: $1",
        "laggedslavemode": "Paringatan: Laman mungkin indak barisi parubahan tabaru.",
        "readonly": "Basis data dikunci",
        "enterlockreason": "Masuakkan alasan panguncian, tamasuak pakiraan bilo kunci akan dibuka",
        "createacct-reason": "Alasan",
        "createacct-reason-ph": "Manga Sanak mambuek akun lain",
        "createacct-submit": "Buek akun Sanak",
+       "createacct-another-submit": "ګڼون جوړول",
        "createacct-benefit-heading": "{{SITENAME}} dibuek dek urang-urang saroman Sanak.",
        "createacct-benefit-body1": "{{PLURAL:$1|suntiangan}}",
        "createacct-benefit-body2": "{{PLURAL:$1|laman}}",
index adc21c6..b76a4f1 100644 (file)
        "download": "преземи",
        "unwatchedpages": "Ненабљудувани страници",
        "listredirects": "Список на пренасочувања",
-       "listduplicatedfiles": "СпиÑ\81ок Ð½Ð° Ð¿Ð¾Ð´Ð°Ñ\82оÑ\82еки Ñ\81о Ð´Ñ\83пликаÑ\82и",
+       "listduplicatedfiles": "СпиÑ\81ок Ð½Ð° Ð´Ñ\83плиÑ\80ани Ð¿Ð¾Ð´Ð°Ñ\82оÑ\82еки",
        "listduplicatedfiles-summary": "Ова е список на податотеки чија најнова верзија е дупликат на најнова верзија на некоја друга податотека. Се земаат предвид само месни податотеки.",
        "listduplicatedfiles-entry": "[[:File:$1|$1]] има [[$3|{{PLURAL:$2|дупликат|$2 дупликати}}]].",
        "unusedtemplates": "Неискористени шаблони",
        "version-libraries-license": "Лиценца",
        "version-libraries-description": "Опис",
        "version-libraries-authors": "Автори",
-       "redirect": "Ð\9fÑ\80енаÑ\81оÑ\87Ñ\83ваÑ\9aе Ð¿Ð¾ Ð¿Ð¾Ð´Ð°Ñ\82оÑ\82ека, Ñ\81Ñ\82Ñ\80аниÑ\86а, Ð¿Ñ\80еÑ\80абоÑ\82ка Ð¸Ð»Ð¸ Ð½Ð°Ð·Ð½Ð°ÐºÐ° Ð²Ð¾ Ð´Ð½ÐµÐ²Ð½Ð¸ÐºÐ¾Ñ\82",
+       "redirect": "Ð\9fÑ\80енаÑ\81оÑ\87Ñ\83ваÑ\9aе Ð¿Ð¾ Ð½Ð°Ð·Ð½Ð°ÐºÐ° Ð½Ð° Ð¿Ð¾Ð´Ð°Ñ\82оÑ\82ека, ÐºÐ¾Ñ\80иÑ\81ник, Ñ\81Ñ\82Ñ\80аниÑ\86а, Ð¿Ñ\80еÑ\80абоÑ\82ка Ð¸Ð»Ð¸ Ð´Ð½ÐµÐ²Ð½Ð¸Ðº",
        "redirect-summary": "Оваа службена страница пренасочува кон податотека (се задава името), страница (се задава назнаката на преработката или страницата), корисничка странца (се задава бројчената назнака на корисникот) или дневнички запис (се дава назнака на записот). Употреба: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]],  [[{{#Special:Redirect}}/user/101]] или [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Дај",
        "redirect-lookup": "Пребарај:",
index 9f2e935..f65f5df 100644 (file)
        "tog-usenewrc": "Molōloāzqueh in tlapatlaliztli in yancuīc tlapatlaliztli āmapan īhuān in tlachiyaliztli tlapōhualāmapan (monequi JavaScript)",
        "tog-showtoolbar": "Motlaīxtlatīz in tlachihchīhualōni pāntli",
        "tog-editondblclick": "Tiquimpatlāz in zāzanilli intlā ōme tiquimpachoa",
-       "tog-watchcreations": "Moaquiāz in āmatl mā niquinyōcoya īhuān in tlahcuilōlli mā niquinquetza īpan notlachiyaliz",
+       "tog-watchcreations": "Niquintlaliz in tlahcuilolamameh in oniquinchiuh ihuan in tlahcuilolpiyaliztin in oniquinquetz ipan notlachiyaliz",
        "tog-watchdefault": "Moaquiāz āmatl īhuān tlahcuilōlli mā niquinpatla in notlachiyaliz",
        "tog-watchmoves": "Moaquiāz āmatl īhuān tlahcuilōlli mā niquinzaca in notlachiyaliz",
-       "tog-watchdeletion": "Moaquiāz āmatl īhuān tlahcuilōlli mā niquimpolo in notlachiyaliz",
+       "tog-watchdeletion": "Niquintlaliz tlahcuilolamameh ihuan tlahcuilolpiyaliztin in oniquimpoloh ipan notlachiyaliz",
        "tog-minordefault": "Ticmachiyōtīz mochīntīn tlapatlalitzintli ic default",
        "tog-previewontop": "Tiquittāz achtochīhualiztli achtopa tlapatlaliztli caxitl",
        "tog-previewonfirst": "Xiquitta achtochīhualiztli inic cē tlapatlalizpan",
@@ -47,7 +47,7 @@
        "tog-diffonly": "Ahmo tiquittāz zāzanilli ītlapiyaliz ahneneuhquilitzīntlan",
        "tog-showhiddencats": "Mà monèxtìkàn in tlatlatìltìn tlaìxmatkàtlàlilòmë",
        "underline-always": "Mochipa",
-       "underline-never": "Aīc",
+       "underline-never": "Aic",
        "editfont-monospace": "Cencoyahualiztli machiyotlahtoliztli",
        "editfont-sansserif": "Sans-serif machiyotlahtoliztli",
        "editfont-serif": "Serif machiyotlahtoliztli",
@@ -78,7 +78,7 @@
        "november": "11 Metz",
        "december": "12 Metz",
        "january-gen": "Ic cē mētztli",
-       "february-gen": "Īcōmemētztli",
+       "february-gen": "Icomemetztli",
        "march-gen": "Īcyēyimētztli",
        "april-gen": "Ic nauhtetl metztli",
        "may-gen": "Īcmācuīllimētztli",
        "categorypage": "Tiquittaz neneuhcayotl itlahcuilolamauh",
        "viewtalkpage": "Xiquitta tēixnāmiquiliztli zāzanilli",
        "otherlanguages": "Occequintin tlahtlahtolcopa",
-       "redirectedfrom": "(Ōmotlacuep īhuīcpa $1)",
+       "redirectedfrom": "(Omocuep ihuicpa $1)",
        "redirectpagesub": "Ōmotlacuep zāzanilli",
-       "lastmodifiedat": "Inin tlahcuilolli omopatlac immanin $1, ipan $2.",
+       "lastmodifiedat": "Inin tlahcuilolamatl omopatlac immanin $1, ipan $2.",
        "viewcount": "Inīn zāzanilli quintlapōhua {{PLURAL:$1|cē tlahpololiztli|$1 tlahpololiztli}}.",
        "protectedpage": "Ōmoquīxtix zāzanilli",
        "jumpto": "Ticholoz ihuicpa:",
        "mainpage": "Yacatlahcuilolli",
        "mainpage-description": "Yacatlahcuilolli",
        "policy-url": "Project:Nahuatīltōn",
-       "portal": "Yacatlahcuilolli tocalpol",
-       "portal-url": "Project:Yacatlahcuilolli tocalpol",
+       "portal": "Necentlaliloyan",
+       "portal-url": "Project:Necentlaliloyan",
        "privacy": "Tlahcuilolli piyaliznahuatilli",
        "privacypage": "Project:Tlahcuilōlpiyaliztechcopa nahuatīltōn",
        "badaccess": "Tlahuelītiliztechcopa ahcuallōtl",
        "retypenew": "Occeppa xiquihcuiloa yancuīc motlahtōlichtacayo:",
        "resetpass_submit": "Xicpatlāz motlahtōlichtacāyo auh xicalaquīz",
        "changepassword-success": "Moichtacātlahtōl ōmopatlac.",
+       "botpasswords-label-cancel": "Moxitiniz",
        "resetpass_forbidden": "Tlahtōlichtacayōtl ahmo mohuelītih mopatlah",
        "resetpass-submit-loggedin": "Ticpatlāz motlahtōlichtacāyo",
-       "resetpass-submit-cancel": "Xiccahua",
+       "resetpass-submit-cancel": "Moxitiniz",
        "passwordreset-username": "Tequihuihcātōcāitl:",
-       "bold_sample": "Tliltic tlahcuilolpiyaliz",
-       "bold_tip": "Tlīltic tlahcuilōlli",
+       "bold_sample": "Tliltic tlahcuiloliztli",
+       "bold_tip": "Tliltic tlahcuiloliztli",
        "italic_sample": "Nacacic tlahcuiloliztli",
        "italic_tip": "Nacacic tlahcuiloliztli",
        "link_sample": "Tzonhuiliztli ītōcā",
        "templatesused": "{{PLURAL:$1|Nemachiotl tlen motequiuhtia|Nemachiomeh tlen moquintequiuhtiah}} ipan inin tlahcuilolamatl:",
        "templatesusedpreview": "{{PLURAL:$1|Nemachiotl tlen motequiuhtia|Nemachiomeh tlen moquintequiuhtiah}} ipan inin achtochihualiztli:",
        "templatesusedsection": "{{PLURAL:$1|Nemachiotl tlen motequiuhtia|Nemachiomeh tlen moquintequiuhtiah}} ipan inin tlaxeloliztli:",
-       "template-protected": "(ōmoquīxti)",
+       "template-protected": "(ahmo moquixtia)",
        "hiddencategories": "Inin tlahcuilolli pohui {{PLURAL:$1|1 tlatlalilli neneuhcayotl|$1 tlatlaliltin neneuhcayomeh}}:",
        "nocreatetext": "Inin huiqui oquitzacuili ic mochihua yancuic tlahcuilolamatl. Quil ticcuepaznequi auh ticpatlaz occe tlahcuilolamatl, [[Special:UserLogin|xicalaqui nozo xicchihua ce cuentah]].",
        "nocreate-loggedin": "Ahmo hueli ticchihua yancuic tlahcuilolamatl.",
        "last": "xocoyoc",
        "page_first": "achto",
        "page_last": "xōcoyōc",
-       "history-fieldset-title": "Xitlatēmo īpan tlahtōllōtl",
+       "history-fieldset-title": "Xitlatemo ihtic tlahtollotl",
        "history-show-deleted": "Zan tlapololtin",
        "histfirst": "in achto",
        "histlast": "in tlatzaucticah",
        "searchprofile-advanced": "Huehca ōmpa",
        "searchprofile-articles-tooltip": "Tictēmōz īpan $1",
        "searchprofile-images-tooltip": "motemoz tlapiyaliztecpaliztli",
-       "searchprofile-everything-tooltip": "Tictēmōz mochi tlapiyalizpan (mopiyah tēixnāmiquiliztli zāzanilli)",
-       "search-result-size": "$1 ({{PLURAL:$2|1 tlahtōl|$2 tlahtōltin}})",
+       "searchprofile-everything-tooltip": "Tictemoz ipan mochi tlapiyaliztli (noihuan tlahcuilolamatl iteixnamiquiliz)",
+       "search-result-size": "$1 ({{PLURAL:$2|1 tlahtol|$2 tlahtoltin}})",
        "search-redirect": "(ixquichca ompa mitzhuica $1)",
        "search-section": "(tlahtōltzintli $1)",
        "search-category": "(neneuhcayotl $1)",
        "recentchanges": "Yancuic tlapatlaliztli",
        "recentchanges-legend": "Yancuīc tlapatlaliztechcopa tlanequiliztli",
        "recentchanges-summary": "Tictoquiliz itlapatlaliz oc yancuic inahuac huiqui inin tlahcuilolpan.",
-       "recentchanges-label-newpage": "Inīn tlapatlaliztli ōquiyōcox cē yancuīc āmatl",
+       "recentchanges-label-newpage": "Inin tlapatlaliztli oquiyocox ce yancuic tlahcuilolamatl",
        "recentchanges-label-minor": " Inin tepiton tlapatlaliztli",
        "recentchanges-label-bot": "Inin tlapaltlaliztli oquichiuh ce robot",
+       "rcfilters-savedqueries-cancel-label": "Moxitiniz",
        "rclistfrom": "Xiquittaz yancuic tlapatlaliztli ixquichca $3 ihuicpa $2",
        "rcshowhideminor": "$1 tlapatlalitzintli",
        "rcshowhideminor-show": "Xicnexti",
        "sourceurl": "Mēyal-URL:",
        "destfilename": "Tōcāhuīc:",
        "watchthisupload": "Tictlachiyaz inin tecpanaliztlapiyaliztli",
+       "upload-dialog-button-cancel": "Moxitiniz",
        "upload-form-label-infoform-name": "Tōcāitl",
        "upload-form-label-usage-filename": "Ihcuilōlli ītōcā",
        "upload_source_file": "(ticpepenaz ce tlahcuilolli mochiuhpohualhuazco)",
        "listfiles-latestversion-yes": "Quēmah",
        "listfiles-latestversion-no": "Ahmō",
        "file-anchor-link": "Tlapiyaliztecpanaliztli",
-       "filehist": "Ihcuilōlli ītlahtōllo",
+       "filehist": "Tlahcuilolli itlahtollo",
        "filehist-deleteall": "tiquimpolōz mochīntīn",
        "filehist-deleteone": "xicpolo",
        "filehist-revert": "tlacuepāz",
        "filedelete-edit-reasonlist": "Xiquihto ipampa ticpohpoloznequi in",
        "mimesearch": "MIME tlatemoliztli",
        "mimetype": "MIME iuhcāyōtl:",
-       "download": "tictemōz",
+       "download": "tictemoz",
        "unwatchedpages": "Zāzaniltin ahmo motlachiya",
        "listredirects": "Tlacuepaliztli",
        "unusedtemplates": "Nemachiyōtīlli ahmotequitiltiah",
        "newpages": "Yancuic tlahcuiloltin",
        "newpages-username": "Tlatequitiltilīltōcāitl:",
        "ancientpages": "Huehcauh tlahcuilolamatl",
-       "move": "Ticzacāz",
+       "move": "Ticzacaz",
        "movethispage": "Ticzacāz inīn zāzanilli",
        "pager-newer-n": "{{PLURAL:$1|1 yancuic|$1 yancuicqueh}}",
        "pager-older-n": "{{PLURAL:$1|1 huehcauh|$1 huehcauhqueh}}",
        "mywatchlist": "Notlachiyaliz",
        "watchnologin": "Ahmo ōtimocalac",
        "removedwatchtext": "Zāzanilli \"[[:$1]]\" ōmopolo [[Special:Watchlist|motlachiyalizco]].",
-       "watch": "Tictlachiyāz",
+       "watch": "Titlachiyaz",
        "watchthispage": "Tictlachiyāz inīn zāzanilli",
        "unwatch": "Ahmo titlachiyaz",
        "watchlist-details": "{{PLURAL:$1|$1 zāzanilli|$1 zāzaniltin}} motlachiyaliz, ahmo mopōhua tēixnāmiquiliztli.",
        "protect-expiry-options": "1 hora:1 hour,1 tonalli:1 day,1 chicueyilhuitl:1 week,2 chicueyilhuitl:2 weeks,1 metztli:1 month,3 metztli:3 months,6 metztli:6 months,1 xihuitl:1 year,mochipa:infinite",
        "restriction-type": "Temacahualiztli:",
        "restriction-edit": "xicpatla",
-       "restriction-move": "Ticzacāz",
+       "restriction-move": "Ticzacaz",
        "restriction-create": "Ticchīhuāz",
        "restriction-upload": "Tlahcuilōlquetza",
        "undelete": "Tiquimittaz tlahcuilolamameh tlen omopohpolohqueh",
        "contributions": "In {{GENDER:$1|tlatequitiltilīlli}} ītlahcuilōl",
        "contributions-title": "Tlatequitiltilīlli $1 ītlahcuilōl",
        "mycontris": "Notlahcuilol",
-       "contribsub2": "$1 ($2)",
+       "contribsub2": "Ihuicpa {{GENDER:$3|$1}} ($2)",
        "uctop": "(axcan tlapatlaliztli)",
-       "month": "Īhuīcpa mētztli (auh achtopa):",
-       "year": "Xiuhhuīcpa (auh achtopa):",
+       "month": "Metzpan (auh yeppa):",
+       "year": "Xiuhpan (auh yeppa):",
        "sp-contributions-newbies": "Tiquinttāz zan yancuīc tlatequitiltilīlli īntlapatlaliz",
        "sp-contributions-newbies-sub": "Ic yancuīc",
        "sp-contributions-newbies-title": "Yancuīc tlatequitiltilīlli ītlahcuilōl",
        "change-blocklink": "Ticpatlaz tlatzacualli",
        "contribslink": "tlapatlaliztli",
        "blocklogpage": "Tlatequitiltilīlli ōmotzacuili",
-       "move-page": "Ticzacāz $1",
+       "move-page": "Ticzacaz $1",
        "move-page-legend": "Tictocapatlaliz inin tlahcuilolamatl",
        "movepagetext": "Nicān mohcuiloa quemeh ticzacāz cē zāzanilli auh mochi in ītlahcuillōloh īhuīc occē yancuīc ītōca.\nHuēhuehtōcāitl yez tlacuepaliztli yancuīc tōcāhuīc.\nTzonhuiliztli huēhuehzāzanilhuīc ahmo mopatlāz.\nXiquitta ic māca xicchīhua [[Special:DoubleRedirects|ōntlacuepaliztli]] ahnozo [[Special:BrokenRedirects|tzomoc]].\nTitzonhuilizpiyāz.\n\nXicmati in zāzanilli ahmo mozacāz intlā ye ia cē zāzanilli tōcātica, zan cah iztāc zāzanilli ahnozo tlacuepaliztli īca ahmo tlahcuilōlloh.\nQuihtōznequi tihuelītīz ticuepāz cē zāzanilli īhuīc ītlācatōca intlā ahcuallōtl ticchīhuāz, tēl ahmo tihuelītīz occeppa tihcuilōz īpan zāzanilli tlein ia.\n\n'''¡XICPŌHUA!'''\nHueliz cah inīn huēyi tlapatlaliztli. Timitztlātlauhtia ticmatīz cuallōtl auh ahcuallōtl achtopa ticzacāz.",
        "movenotallowed": "Ahmo tihuelīti tiquinzaca zāzaniltin.",
        "allmessages-filter-all": "Mochi",
        "allmessages-language": "Tlahtolli:",
        "allmessages-filter-submit": "Tiyaz",
-       "thumbnail-more": "Tiquihuēyiyāz",
+       "thumbnail-more": "Tichueyiyaz",
        "thumbnail_error": "Aiuhcāyōtl ihcuāc mochīhuaya tepitōntli: $1",
        "import": "Tiquincōhuāz zāzaniltin",
        "import-interwiki-sourcewiki": "Mēyalhuiqui:",
        "tooltip-ca-edit": "Ticpatlaz inin tlahcuilolli",
        "tooltip-ca-addsection": "Ticpehualiz ce yancuic xeliuhcayotl.",
        "tooltip-ca-viewsource": "Inīn zāzanilli ōmoquīxti. Tihuelīti tiquitta ītlahtōlcaquiliztilōni.",
-       "tooltip-ca-history": "Achtopa āxcān zāzanilli īhuān in tlatequitiltilīlli ōquinchīuhqueh",
+       "tooltip-ca-history": "In tlein ye oquichiuhqueh ipan inin tlahcuilolamatl",
        "tooltip-ca-protect": "Ticquīxtiāz inīn zāzanilli",
        "tooltip-ca-delete": "Ticpolōz inīn zāzanilli",
        "tooltip-ca-undelete": "Ahticpolōz inīn zāzanilli",
        "tooltip-ca-move": "Ticzacaz inin tlahcuilolamatl",
-       "tooltip-ca-watch": "Ticcentiliz inin tlahtolli motecpanaliz",
+       "tooltip-ca-watch": "Tictlaliz inin tlahcuilolamatl motlachiyaliz",
        "tooltip-ca-unwatch": "Ticpohpoloz inin tlahcuilolamatl ipan motlachiyaliz",
        "tooltip-search": "Tlatemoliztli ipan {{SITENAME}}",
        "tooltip-search-go": "Tiyaz ihuicpa tlahcuilolamatl ica inin huel melahuac tocaitl intla oncah",
        "tooltip-p-logo": "Tiquittaz in yacatlahcuilolli",
        "tooltip-n-mainpage": "Tiquittaz in yacatlahcuilolli",
        "tooltip-n-mainpage-description": "Tiquittaz in yacatlahcuilolli",
-       "tooltip-n-portal": "Tlachīhualiztechcopa, inōn tihuelīti titlachīhua, tlatēmoyān",
+       "tooltip-n-portal": "Itech totequitiliz, in canin titlachihua, in canin titlatemoa",
        "tooltip-n-recentchanges": "Iyancuictlapatlalizhuan ipan huiqui",
        "tooltip-n-randompage": "Tiquittaz cecen tlahcuilolli",
-       "tooltip-n-help": "In tēmachtīlōyān",
+       "tooltip-n-help": "In canin ticmachtiz",
        "tooltip-t-whatlinkshere": "Mochintin tlahcuiloltin huiquipan quitzonhuiliah nican",
        "tooltip-t-recentchangeslinked": "Yancuic tlapatlaliztli ipan tlahcuiloltin tlein quitzonhuilia nican",
        "tooltip-feed-rss": "RSS tlachicahualiztli inin tlahcuilolamatl",
        "tooltip-t-print": "Tepoztlahcuilolli",
        "tooltip-ca-nstab-main": "Tiquittaz tlein quipiya in tlahcuilolli",
        "tooltip-ca-nstab-user": "Xiquitta tequitiuhqui itlahcuilolamauh",
-       "tooltip-ca-nstab-special": "Inīn nōncuahquīzqui āmatl, auh ahmohuelitizpatla",
+       "tooltip-ca-nstab-special": "Inin noncuahquizqui amatl, auh ahmohueli in ticpatlaz",
        "tooltip-ca-nstab-project": "Xiquitta in tlayecantequitl itlahcuilolamauh",
-       "tooltip-ca-nstab-image": "Xiquittāz īxipzāzanilli",
+       "tooltip-ca-nstab-image": "Xiquittaz tlahcuilolpiyalli",
        "tooltip-ca-nstab-mediawiki": "Xiquitta in tlahcuilōltzin",
        "tooltip-ca-nstab-template": "Xiquitta in nemachiyōtīlli",
        "tooltip-ca-nstab-help": "Xiquitta in tēpalēhuiliztli zāzanilli",
        "tooltip-summary": "Xiquihcuilo ce tepiton tlahcuiloltontli",
        "anonymous": "Ahtōcāitl {{PLURAL:$1|tlatequitiltilīlli}} īpan {{SITENAME}}",
        "siteuser": "$1 tlatequitiltilīlli īpan {{SITENAME}}",
-       "lastmodifiedatby": "Inin tlahcuilolamatl omopatlac ipan $2, $1 ipal $3.",
+       "lastmodifiedatby": "Inin tlahcuilolamatl omopatlac ipan $2, $1 ipan $3.",
        "others": "occequīntīn",
        "siteusers": "$1 {{PLURAL:$2|{{GENDER:$1|tequitiuhqui}}|tequitiuhqueh}} īpan {{SITENAME}}",
        "spam_reverting": "Mocuepacah īhuīc xōcoyōc tlapatlaliztli ahmo tzonhuilizca īhuīc $1",
        "size-megabytes": "$1 MB",
        "size-gigabytes": "$1 GB",
        "watchlistedit-normal-title": "Ticpatlāz motlachiyaliz",
+       "watchlistedit-raw-titles": "Tlahcuilolamameh",
        "watchlistedit-raw-added": "{{PLURAL:$1|Ōmocentili cē zāzanilli|Ōmocentilih $1 zāzaniltin}}:",
+       "watchlistedit-clear-titles": "Tocaitl",
        "watchlisttools-view": "Tiquinttāz huēyi tlapatlaliztli",
        "watchlisttools-edit": "Tiquittāz auh ticpatlāz motlachiyaliz",
        "version": "Machiyōtzin",
        "blankpage": "Iztāc zāzanilli",
        "htmlform-selectorother-other": "Occe",
        "rightsnone": "ahtlein",
+       "feedback-cancel": "Moxitiniz",
        "searchsuggest-search": "Tlatemoliztli",
        "api-error-stashfailed": "Tlâtek îtlakawilistli: In tlatèmakani awel òkeuh in èwalpanòni.",
        "api-error-unknown-warning": "Âmò ìxmatkàyo tlanawatilistli: \"$1\".",
index 1f4f82c..d856230 100644 (file)
        "deleting-backlinks-warning": "گواښنه:''' دا مخ چې تاسې يې ړنگوی د [[Special:WhatLinksHere/{{FULLPAGENAME}}|نورو مخونو]] سره تړلی او يا هم په نورو مخونو کې نغاړل شوی دی.",
        "rollbacklink": "په شابېول",
        "rollbacklinkcount": "$1 {{PLURAL:$1|سمون|سمونونه}} پرشابېول",
+       "rollbacklinkcount-morethan": "تر $1 زيات {{PLURAL:$1|بدلون|بدلونونه}} پرشا بوځي",
        "editcomment": "د سمون لنډيز دا وو: \"''$1''\".",
        "changecontentmodel-title-label": "مخ سرليک",
        "changecontentmodel-model-label": "د نوي مېنځپانگې موډل",
        "limitreport-expansiondepth-value": "$1/$2",
        "limitreport-expensivefunctioncount": "د قیمتي پارسير فعالیت شمیرې",
        "limitreport-expensivefunctioncount-value": "$1/$2",
+       "limitreport-unstrip-size-value": "$1/$2 {{PLURAL:$2|ټکۍ|ټکي}}",
        "expandtemplates": "کينډۍ غځول",
        "expand_templates_intro": "په دا ځانګړي مخ کي متن پاڼه ترلاسه کیږي کوم چي په ټول ډوله مخونو کي کارول کیږي دلته دا مخ بيا بیا وده کوي. د تحلیل دندو لکه <code><nowiki>{{</nowiki>#language:…}}</code> او متغیرونه لکه <code><nowiki>{{</nowiki>CURRENTDAY}}</code> هم سره نښلوي — په واقعیت کې، د ډلو دننه هر څه. دا خپله د ميډياويکي په اړونده مرحله کولو سره ترسره کيږي.",
        "expand_templates_title": "د موزوع سرليک، د {{FULLPAGENAME}} لپاره او نور:",
        "restrictionsfield-badip": "ناباوره آي پي  آدرس او حدود د : $1",
        "restrictionsfield-label": "اجازه ورکړل شوي آي پي حدودونه:",
        "restrictionsfield-help": "په هر کرښه کې د اي پي پته یا د سینیر رینټ داخل کړئ. د هر شی فعالولو لپاره دا ارزښت وکاروئ: <code>0.0.0.0/0</code><br /><code>::/0</code>",
+       "edit-error-short": "تيروتنه: $1",
+       "edit-error-long": "تيروتنې:$1",
        "revid": "بیاکتنه $1",
        "pageid": "د مخ پېژند$1",
        "rawhtml-notallowed": "لیبلونه &lt;html&gt; د منظمو ليکنو څخه بهر نشي کارول کیدی.",
index 5b8dc62..0a17742 100644 (file)
        "copyrightwarning": "ياد رکندا ته {{SITENAME}} لاءِ سموريون ڀاڱيداريون $2 تحت پڌريون ڪجن ٿيون (تفصيلن لاءِ $1 ڏسندا). اوهان جي تحرير کي {{SITENAME}} جي قائدن تحت ترميمي سگهجي ٿو. جيڪڏهن اوهان نه ٿا چاهيو ته اوهان جي لکڻين کي بي رحميءَ سان ترميميو وڃي يا ورهائي عام ڪيو وڃي ته پوءِ پنهنجي لکڻي هتي جمع نه ڪرايو. پنهنجو مواد هتي جمع ڪرڻ جو مطلب هوندو ته توهان کي جمع ڪرايل مواد جي مفت فراهمي ۽ کُليل تبديليءَ تي ڪو به اعتراز ناهي.<br />\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو ته توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن مفت وسيلي تان ڪاپي ڪيو آهي.\n'''تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ کان سواءِ هتي جمع نه ڪريو.'''",
        "copyrightwarning2": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيدارين کي ٻيا ڀاڱيدار سنواري، بدلائي، يا ڊاهي سگھن ٿا. جيڪڏهن اوهان نہ ٿا چاهيو تہ اوهان جي لکڻين کي بي رحميءَ سان ترميميو وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ پنهنجي لکڻي هتي جمع نہ ڪرايو.</br>\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو تہ توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن اهڙي ئي مفت عوامي وسيلي تان ڪاپي ڪيو آهي. (تفصيلن لاءِ $1 ڏسندا).\n\n<strong>تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ بنان هتي جمع نہ ڪريو.</strong>",
        "protectedpagewarning": "<strong>چتاءُ: هيءَ صفحو اهڙيءَ ريت تحفظيو ويو آهي جو فقط منتظمين ئي ان کي سنواري سگھن ٿا. </strong>\nتازه ترين لاگ حوالي طور پيش ڪجي ٿو:",
-       "semiprotectedpagewarning": "<strong>نوٽ:</strong> هيءَ صفحو اهڙيءَ ريت تحفظيو ويو آهي جو فقط کاتيدار واپرائيندڙ ئي ان کي سنواري سگھن ٿا.\nتازه ترين لاگ حوالي طور پيش ڪجي ٿو:",
+       "semiprotectedpagewarning": "<strong>نوٽ:</strong> هيءَ صفحو اهڙيءَ ريت تحفظيو ويو آهي جو فقط خودڪار نموني پڪ ڪيل واپرائيندڙ ئي ان کي سنواري سگھن ٿا.\nتازه ترين لاگ حوالي طور پيش ڪجي ٿو:",
        "templatesused": "هن صفحي تي استعمال ٿيندڙ {{PLURAL:$1|سانچو|سانچا}}:",
        "templatesusedpreview": "هن پيش نگاھ ۾ استعمال ٿيل {{PLURAL:$1|سانچو|سانچا}}:",
        "templatesusedsection": "هن سيڪشن ۾ استعمال ٿيل {{PLURAL:$1|سانچو|سانچا}}:",
        "recentchangesdays": "تازين تبديلين ۾ ڏيکارڻ جي لاءِ ڏينهن:",
        "recentchangesdays-max": "وڌ ۾ وڌ $1 {{PLURAL:$1|ڏينهن}}",
        "recentchangescount": "عدم پيروي جي صورت ۾ ڏيکارڻ جي لاءِ ترميمون:",
-       "prefs-help-recentchangescount": "ان ۾ تازيون تبديليون، صفحن جي سوانح، ۽ لاگ شامل آهن.",
+       "prefs-help-recentchangescount": "وڌ ۾ وڌ انگ: 1000",
        "savedprefs": "توھان جون ترجيحون سانڍجي چڪيون آھن.",
        "savedrights": "{{GENDER:$1|$1}} جا واپرائيندڙ گروھ سانڍجي چڪا آھن.",
        "timezonelegend": "اوقاتي زون:",
index 5cda7e8..6fe93cb 100644 (file)
        "pt-login-button": "ⴽⵛⵎ",
        "pt-createaccount": "ⵙⵏⴼⵍⵓⵍ ⴰⵎⵉⴹⴰⵏ",
        "pt-userlogout": "ⴼⴼⵖ",
+       "newpassword": "ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ ⵜⴰⵎⴰⵢⵏⵓⵜ",
        "botpasswords-label-create": "ⵙⵏⵓⵍⴼⵓ",
        "botpasswords-label-delete": "ⴽⴽⵙ",
        "passwordreset": "ⵔⴰⵔ ⴷ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ",
+       "changeemail-newemail": "ⵉⵎⴰⵢⵍ ⴰⵎⴰⵢⵏⵓ:",
        "bold_sample": "ⴰⴹⵔⵉⵙ ⴰⵣⵓⵔⴰⵔ",
        "bold_tip": "ⴰⴹⵔⵉⵙ ⴰⵣⵓⵔⴰⵔ",
        "italic_sample": "ⴰⴹⵔⵉⵙ ⵓⵣⵍⵉⴳ",
diff --git a/languages/messages/MessagesAbs.php b/languages/messages/MessagesAbs.php
new file mode 100644 (file)
index 0000000..8cbe79b
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+/** Ambonese Malay (Bahasa Ambon)
+ *
+ * To improve a translation please visit https://translatewiki.net
+ *
+ * @ingroup Language
+ * @file
+ *
+ */
+
+$fallback = 'id';
index b9a7ccc..7b779a6 100644 (file)
@@ -6,7 +6,7 @@
     "doc": "jsduck",
     "postdoc": "grunt copy:jsduck",
     "selenium": "./tests/selenium/selenium.sh",
-    "selenium-test": "grunt webdriver:test"
+    "selenium-test": "wdio ./tests/selenium/wdio.conf.js"
   },
   "devDependencies": {
     "bluebird": "3.5.1",
@@ -21,7 +21,6 @@
     "grunt-jsonlint": "1.1.0",
     "grunt-karma": "2.0.0",
     "grunt-stylelint": "0.10.0",
-    "grunt-webdriver": "2.0.3",
     "karma": "1.7.1",
     "karma-chrome-launcher": "2.2.0",
     "karma-firefox-launcher": "1.0.1",
index d3e1b65..160aeb3 100644 (file)
@@ -113,6 +113,20 @@ return [
                ],
        ],
 
+       'jquery.tablesorter.styles' => [
+               'targets' => [ 'desktop', 'mobile' ],
+               'styles' => [
+                       'resources/src/jquery/jquery.tablesorter.styles.less',
+               ],
+       ],
+       'jquery.makeCollapsible.styles' => [
+               'targets' => [ 'desktop', 'mobile' ],
+               'class' => ResourceLoaderLessVarFileModule::class,
+               'styles' => [
+                       'resources/src/jquery/jquery.makeCollapsible.styles.less',
+               ],
+       ],
+
        'mediawiki.skinning.content.parsoid' => [
                // Style Parsoid HTML+RDFa output consistent with wikitext from PHP parser
                // with the interface.css styles; skinStyles should be used if your
@@ -277,6 +291,7 @@ return [
                'scripts' => 'resources/src/jquery/jquery.localize.js',
        ],
        'jquery.makeCollapsible' => [
+               'dependencies' => [ 'jquery.makeCollapsible.styles' ],
                'scripts' => 'resources/src/jquery/jquery.makeCollapsible.js',
                'styles' => 'resources/src/jquery/jquery.makeCollapsible.css',
                'messages' => [ 'collapsible-expand', 'collapsible-collapse' ],
@@ -317,6 +332,7 @@ return [
                'styles' => 'resources/src/jquery/jquery.tablesorter.less',
                'messages' => [ 'sort-descending', 'sort-ascending' ],
                'dependencies' => [
+                       'jquery.tablesorter.styles',
                        'mediawiki.RegExp',
                        'mediawiki.language.months',
                ],
@@ -712,7 +728,7 @@ return [
        'moment' => [
                'scripts' => [
                        // HACK: For some reason if you don't define window.moment first, loading moment fatals
-                       'resources/src/moment-global.js',
+                       'resources/src/moment/moment-global.js',
                        'resources/lib/moment/moment.js',
                ],
                'languageScripts' => [
@@ -739,7 +755,7 @@ return [
                        'de-ch' => 'resources/lib/moment/locale/de-ch.js',
                        'dv' => 'resources/lib/moment/locale/dv.js',
                        'el' => 'resources/lib/moment/locale/el.js',
-                       'en' => 'resources/src/moment-dmy.js',
+                       'en' => 'resources/src/moment/moment-dmy.js',
                        'en-au' => 'resources/lib/moment/locale/en-au.js',
                        'en-ca' => 'resources/lib/moment/locale/en-ca.js',
                        'en-gb' => 'resources/lib/moment/locale/en-gb.js',
@@ -827,7 +843,7 @@ return [
                // after locale definitions
                'skinScripts' => [
                        'default' => [
-                               'resources/src/moment-locale-overrides.js',
+                               'resources/src/moment/moment-locale-overrides.js',
                        ],
                ],
                'dependencies' => [
@@ -1197,11 +1213,6 @@ return [
                        'mediawiki.api',
                ],
        ],
-       'mediawiki.sectionAnchor' => [
-               // Back-compat to hide it on cached pages (T18691; Ie9e334e973; 2015-03-17)
-               'styles' => 'resources/src/mediawiki/mediawiki.sectionAnchor.css',
-               'targets' => [ 'desktop', 'mobile' ],
-       ],
        'mediawiki.storage' => [
                'scripts' => 'resources/src/mediawiki/mediawiki.storage.js',
                'targets' => [ 'desktop', 'mobile' ],
@@ -1616,7 +1627,10 @@ return [
        ],
 
        'mediawiki.libs.pluralruleparser' => [
-               'scripts' => 'resources/src/mediawiki.libs/CLDRPluralRuleParser.js',
+               'scripts' => [
+                       'resources/lib/CLDRPluralRuleParser/CLDRPluralRuleParser.js',
+                       'resources/src/mediawiki.libs.pluralruleparser/export.js',
+               ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
 
@@ -1656,7 +1670,10 @@ return [
        /* MediaWiki Libs */
 
        'mediawiki.libs.jpegmeta' => [
-               'scripts' => 'resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js',
+               'scripts' => [
+                       'resources/src/mediawiki.libs.jpegmeta/jpegmeta.js',
+                       'resources/src/mediawiki.libs.jpegmeta/export.js',
+               ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
 
diff --git a/resources/lib/CLDRPluralRuleParser/CLDRPluralRuleParser.js b/resources/lib/CLDRPluralRuleParser/CLDRPluralRuleParser.js
new file mode 100644 (file)
index 0000000..1491e3d
--- /dev/null
@@ -0,0 +1,608 @@
+/**
+ * cldrpluralparser.js
+ * A parser engine for CLDR plural rules.
+ *
+ * Copyright 2012-2014 Santhosh Thottingal and other contributors
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ * @version 0.1.0
+ * @source https://github.com/santhoshtr/CLDRPluralRuleParser
+ * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
+ * @author Timo Tijhof
+ * @author Amir Aharoni
+ */
+
+/**
+ * Evaluates a plural rule in CLDR syntax for a number
+ * @param {string} rule
+ * @param {integer} number
+ * @return {boolean} true if evaluation passed, false if evaluation failed.
+ */
+
+// UMD returnExports https://github.com/umdjs/umd/blob/master/returnExports.js
+(function(root, factory) {
+       if (typeof define === 'function' && define.amd) {
+               // AMD. Register as an anonymous module.
+               define(factory);
+       } else if (typeof exports === 'object') {
+               // Node. Does not work with strict CommonJS, but
+               // only CommonJS-like environments that support module.exports,
+               // like Node.
+               module.exports = factory();
+       } else {
+               // Browser globals (root is window)
+               root.pluralRuleParser = factory();
+       }
+}(this, function() {
+
+function pluralRuleParser(rule, number) {
+       'use strict';
+
+       /*
+       Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
+       -----------------------------------------------------------------
+       condition     = and_condition ('or' and_condition)*
+               ('@integer' samples)?
+               ('@decimal' samples)?
+       and_condition = relation ('and' relation)*
+       relation      = is_relation | in_relation | within_relation
+       is_relation   = expr 'is' ('not')? value
+       in_relation   = expr (('not')? 'in' | '=' | '!=') range_list
+       within_relation = expr ('not')? 'within' range_list
+       expr          = operand (('mod' | '%') value)?
+       operand       = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
+       range_list    = (range | value) (',' range_list)*
+       value         = digit+
+       digit         = 0|1|2|3|4|5|6|7|8|9
+       range         = value'..'value
+       samples       = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
+       sampleRange   = decimalValue '~' decimalValue
+       decimalValue  = value ('.' value)?
+       */
+
+       // We don't evaluate the samples section of the rule. Ignore it.
+       rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
+
+       if (!rule.length) {
+               // Empty rule or 'other' rule.
+               return true;
+       }
+
+       // Indicates the current position in the rule as we parse through it.
+       // Shared among all parsing functions below.
+       var pos = 0,
+               operand,
+               expression,
+               relation,
+               result,
+               whitespace = makeRegexParser(/^\s+/),
+               value = makeRegexParser(/^\d+/),
+               _n_ = makeStringParser('n'),
+               _i_ = makeStringParser('i'),
+               _f_ = makeStringParser('f'),
+               _t_ = makeStringParser('t'),
+               _v_ = makeStringParser('v'),
+               _w_ = makeStringParser('w'),
+               _is_ = makeStringParser('is'),
+               _isnot_ = makeStringParser('is not'),
+               _isnot_sign_ = makeStringParser('!='),
+               _equal_ = makeStringParser('='),
+               _mod_ = makeStringParser('mod'),
+               _percent_ = makeStringParser('%'),
+               _not_ = makeStringParser('not'),
+               _in_ = makeStringParser('in'),
+               _within_ = makeStringParser('within'),
+               _range_ = makeStringParser('..'),
+               _comma_ = makeStringParser(','),
+               _or_ = makeStringParser('or'),
+               _and_ = makeStringParser('and');
+
+       function debug() {
+               // console.log.apply(console, arguments);
+       }
+
+       debug('pluralRuleParser', rule, number);
+
+       // Try parsers until one works, if none work return null
+       function choice(parserSyntax) {
+               return function() {
+                       var i, result;
+
+                       for (i = 0; i < parserSyntax.length; i++) {
+                               result = parserSyntax[i]();
+
+                               if (result !== null) {
+                                       return result;
+                               }
+                       }
+
+                       return null;
+               };
+       }
+
+       // Try several parserSyntax-es in a row.
+       // All must succeed; otherwise, return null.
+       // This is the only eager one.
+       function sequence(parserSyntax) {
+               var i, parserRes,
+                       originalPos = pos,
+                       result = [];
+
+               for (i = 0; i < parserSyntax.length; i++) {
+                       parserRes = parserSyntax[i]();
+
+                       if (parserRes === null) {
+                               pos = originalPos;
+
+                               return null;
+                       }
+
+                       result.push(parserRes);
+               }
+
+               return result;
+       }
+
+       // Run the same parser over and over until it fails.
+       // Must succeed a minimum of n times; otherwise, return null.
+       function nOrMore(n, p) {
+               return function() {
+                       var originalPos = pos,
+                               result = [],
+                               parsed = p();
+
+                       while (parsed !== null) {
+                               result.push(parsed);
+                               parsed = p();
+                       }
+
+                       if (result.length < n) {
+                               pos = originalPos;
+
+                               return null;
+                       }
+
+                       return result;
+               };
+       }
+
+       // Helpers - just make parserSyntax out of simpler JS builtin types
+       function makeStringParser(s) {
+               var len = s.length;
+
+               return function() {
+                       var result = null;
+
+                       if (rule.substr(pos, len) === s) {
+                               result = s;
+                               pos += len;
+                       }
+
+                       return result;
+               };
+       }
+
+       function makeRegexParser(regex) {
+               return function() {
+                       var matches = rule.substr(pos).match(regex);
+
+                       if (matches === null) {
+                               return null;
+                       }
+
+                       pos += matches[0].length;
+
+                       return matches[0];
+               };
+       }
+
+       /**
+        * Integer digits of n.
+        */
+       function i() {
+               var result = _i_();
+
+               if (result === null) {
+                       debug(' -- failed i', parseInt(number, 10));
+
+                       return result;
+               }
+
+               result = parseInt(number, 10);
+               debug(' -- passed i ', result);
+
+               return result;
+       }
+
+       /**
+        * Absolute value of the source number (integer and decimals).
+        */
+       function n() {
+               var result = _n_();
+
+               if (result === null) {
+                       debug(' -- failed n ', number);
+
+                       return result;
+               }
+
+               result = parseFloat(number, 10);
+               debug(' -- passed n ', result);
+
+               return result;
+       }
+
+       /**
+        * Visible fractional digits in n, with trailing zeros.
+        */
+       function f() {
+               var result = _f_();
+
+               if (result === null) {
+                       debug(' -- failed f ', number);
+
+                       return result;
+               }
+
+               result = (number + '.').split('.')[1] || 0;
+               debug(' -- passed f ', result);
+
+               return result;
+       }
+
+       /**
+        * Visible fractional digits in n, without trailing zeros.
+        */
+       function t() {
+               var result = _t_();
+
+               if (result === null) {
+                       debug(' -- failed t ', number);
+
+                       return result;
+               }
+
+               result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
+               debug(' -- passed t ', result);
+
+               return result;
+       }
+
+       /**
+        * Number of visible fraction digits in n, with trailing zeros.
+        */
+       function v() {
+               var result = _v_();
+
+               if (result === null) {
+                       debug(' -- failed v ', number);
+
+                       return result;
+               }
+
+               result = (number + '.').split('.')[1].length || 0;
+               debug(' -- passed v ', result);
+
+               return result;
+       }
+
+       /**
+        * Number of visible fraction digits in n, without trailing zeros.
+        */
+       function w() {
+               var result = _w_();
+
+               if (result === null) {
+                       debug(' -- failed w ', number);
+
+                       return result;
+               }
+
+               result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
+               debug(' -- passed w ', result);
+
+               return result;
+       }
+
+       // operand       = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
+       operand = choice([n, i, f, t, v, w]);
+
+       // expr          = operand (('mod' | '%') value)?
+       expression = choice([mod, operand]);
+
+       function mod() {
+               var result = sequence(
+                       [operand, whitespace, choice([_mod_, _percent_]), whitespace, value]
+               );
+
+               if (result === null) {
+                       debug(' -- failed mod');
+
+                       return null;
+               }
+
+               debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
+
+               return parseInt(result[0], 10) % parseInt(result[4], 10);
+       }
+
+       function not() {
+               var result = sequence([whitespace, _not_]);
+
+               if (result === null) {
+                       debug(' -- failed not');
+
+                       return null;
+               }
+
+               return result[1];
+       }
+
+       // is_relation   = expr 'is' ('not')? value
+       function is() {
+               var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
+
+               if (result !== null) {
+                       debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10));
+
+                       return result[0] === parseInt(result[4], 10);
+               }
+
+               debug(' -- failed is');
+
+               return null;
+       }
+
+       // is_relation   = expr 'is' ('not')? value
+       function isnot() {
+               var result = sequence(
+                       [expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]
+               );
+
+               if (result !== null) {
+                       debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
+
+                       return result[0] !== parseInt(result[4], 10);
+               }
+
+               debug(' -- failed isnot');
+
+               return null;
+       }
+
+       function not_in() {
+               var i, range_list,
+                       result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
+
+               if (result !== null) {
+                       debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]);
+                       range_list = result[4];
+
+                       for (i = 0; i < range_list.length; i++) {
+                               if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
+                                       return false;
+                               }
+                       }
+
+                       return true;
+               }
+
+               debug(' -- failed not_in');
+
+               return null;
+       }
+
+       // range_list    = (range | value) (',' range_list)*
+       function rangeList() {
+               var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]),
+                       resultList = [];
+
+               if (result !== null) {
+                       resultList = resultList.concat(result[0]);
+
+                       if (result[1][0]) {
+                               resultList = resultList.concat(result[1][0]);
+                       }
+
+                       return resultList;
+               }
+
+               debug(' -- failed rangeList');
+
+               return null;
+       }
+
+       function rangeTail() {
+               // ',' range_list
+               var result = sequence([_comma_, rangeList]);
+
+               if (result !== null) {
+                       return result[1];
+               }
+
+               debug(' -- failed rangeTail');
+
+               return null;
+       }
+
+       // range         = value'..'value
+       function range() {
+               var i, array, left, right,
+                       result = sequence([value, _range_, value]);
+
+               if (result !== null) {
+                       debug(' -- passed range');
+
+                       array = [];
+                       left = parseInt(result[0], 10);
+                       right = parseInt(result[2], 10);
+
+                       for (i = left; i <= right; i++) {
+                               array.push(i);
+                       }
+
+                       return array;
+               }
+
+               debug(' -- failed range');
+
+               return null;
+       }
+
+       function _in() {
+               var result, range_list, i;
+
+               // in_relation   = expr ('not')? 'in' range_list
+               result = sequence(
+                       [expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]
+               );
+
+               if (result !== null) {
+                       debug(' -- passed _in:' + result);
+
+                       range_list = result[5];
+
+                       for (i = 0; i < range_list.length; i++) {
+                               if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
+                                       return (result[1][0] !== 'not');
+                               }
+                       }
+
+                       return (result[1][0] === 'not');
+               }
+
+               debug(' -- failed _in ');
+
+               return null;
+       }
+
+       /**
+        * The difference between "in" and "within" is that
+        * "in" only includes integers in the specified range,
+        * while "within" includes all values.
+        */
+       function within() {
+               var range_list, result;
+
+               // within_relation = expr ('not')? 'within' range_list
+               result = sequence(
+                       [expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]
+               );
+
+               if (result !== null) {
+                       debug(' -- passed within');
+
+                       range_list = result[5];
+
+                       if ((result[0] >= parseInt(range_list[0], 10)) &&
+                               (result[0] < parseInt(range_list[range_list.length - 1], 10))) {
+
+                               return (result[1][0] !== 'not');
+                       }
+
+                       return (result[1][0] === 'not');
+               }
+
+               debug(' -- failed within ');
+
+               return null;
+       }
+
+       // relation      = is_relation | in_relation | within_relation
+       relation = choice([is, not_in, isnot, _in, within]);
+
+       // and_condition = relation ('and' relation)*
+       function and() {
+               var i,
+                       result = sequence([relation, nOrMore(0, andTail)]);
+
+               if (result) {
+                       if (!result[0]) {
+                               return false;
+                       }
+
+                       for (i = 0; i < result[1].length; i++) {
+                               if (!result[1][i]) {
+                                       return false;
+                               }
+                       }
+
+                       return true;
+               }
+
+               debug(' -- failed and');
+
+               return null;
+       }
+
+       // ('and' relation)*
+       function andTail() {
+               var result = sequence([whitespace, _and_, whitespace, relation]);
+
+               if (result !== null) {
+                       debug(' -- passed andTail' + result);
+
+                       return result[3];
+               }
+
+               debug(' -- failed andTail');
+
+               return null;
+
+       }
+       //  ('or' and_condition)*
+       function orTail() {
+               var result = sequence([whitespace, _or_, whitespace, and]);
+
+               if (result !== null) {
+                       debug(' -- passed orTail: ' + result[3]);
+
+                       return result[3];
+               }
+
+               debug(' -- failed orTail');
+
+               return null;
+       }
+
+       // condition     = and_condition ('or' and_condition)*
+       function condition() {
+               var i,
+                       result = sequence([and, nOrMore(0, orTail)]);
+
+               if (result) {
+                       for (i = 0; i < result[1].length; i++) {
+                               if (result[1][i]) {
+                                       return true;
+                               }
+                       }
+
+                       return result[0];
+               }
+
+               return false;
+       }
+
+       result = condition();
+
+       /**
+        * For success, the pos must have gotten to the end of the rule
+        * and returned a non-null.
+        * n.b. This is part of language infrastructure,
+        * so we do not throw an internationalizable message.
+        */
+       if (result === null) {
+               throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
+       }
+
+       if (pos !== rule.length) {
+               debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
+       }
+
+       return result;
+}
+
+return pluralRuleParser;
+
+}));
index 6364c70..fc52d51 100644 (file)
@@ -1,3 +1,9 @@
+/*
+ * Please do not add any CSS rules here that impact the positioning of the element
+ *  e.g. padding, margin, position or float.
+ * These instead should live in jquery.makeCollapsible.styles
+*/
+
 /* See also jquery.makeCollapsible.js */
 .mw-collapsible-toggle {
        float: right;
 .mw-collapsible-toggle-default:after {
        content: ']';
 }
-/* Align the toggle based on the direction of the content language */
-/* @noflip */
-.mw-content-ltr .mw-collapsible-toggle,
-.mw-content-rtl .mw-content-ltr .mw-collapsible-toggle {
-       float: right;
-}
-/* @noflip */
-.mw-content-rtl .mw-collapsible-toggle,
-.mw-content-ltr .mw-content-rtl .mw-collapsible-toggle {
-       float: left;
-}
 
 .mw-customtoggle,
 .mw-collapsible-toggle {
@@ -37,17 +32,3 @@ caption .mw-collapsible-toggle,
 .mw-content-ltr .mw-content-rtl caption .mw-collapsible-toggle {
        float: none;
 }
-
-/* list-items go as wide as their parent element, don't float them inside list items */
-li .mw-collapsible-toggle,
-.mw-content-ltr li .mw-collapsible-toggle,
-.mw-content-rtl li .mw-collapsible-toggle,
-.mw-content-rtl .mw-content-ltr li .mw-collapsible-toggle,
-.mw-content-ltr .mw-content-rtl li .mw-collapsible-toggle {
-       float: none;
-}
-
-/* the added list item should have no list-style */
-.mw-collapsible-toggle-li {
-       list-style: none;
-}
index 1f40e0a..503e3a6 100644 (file)
@@ -1,5 +1,8 @@
 /**
  * jQuery makeCollapsible
+ * Note: To avoid performance issues such as reflows, several styles are
+ * shipped in mediawiki.makeCollapsible.styles to reserve space for the toggle control. Please
+ * familiarise yourself with that CSS before making any changes to this code.
  *
  * Dual licensed:
  * - CC BY 3.0 <http://creativecommons.org/licenses/by/3.0>
                        if ( $collapsible.data( 'mw-made-collapsible' ) ) {
                                return;
                        } else {
-                               $collapsible.data( 'mw-made-collapsible', true );
+                               // Let CSS know that it no longer needs to worry about flash of unstyled content.
+                               // This will allow mediawiki.makeCollapsible.styles to disable temporary pseudo elements, that
+                               // are needed to avoid a flash of unstyled content.
+                               $collapsible.addClass( 'mw-made-collapsible' )
+                                       .data( 'mw-made-collapsible', true );
                        }
 
                        // Use custom text or default?
diff --git a/resources/src/jquery/jquery.makeCollapsible.styles.less b/resources/src/jquery/jquery.makeCollapsible.styles.less
new file mode 100644 (file)
index 0000000..f19c3c2
--- /dev/null
@@ -0,0 +1,125 @@
+/**
+ * These rules prevent re-flows relating to collapsible on-wiki elements (T42812).
+ * This is done by temporarily creating a pseudo element in the place that JavaScript will insert
+ * a toggle control. The same CSS rules that control the positioning of the toggle control will apply
+ * to the pseudo element. When the JavaScript has executed
+ * (See corresponding non-render blocking CSS in jquery.makeCollapsible)
+ * all pseudo elements will be removed.
+ *
+ * Currently we support all the examples on [[mw:Manual:Collapsible_elements/Demo/Simple]]
+ * All examples on [[mw:Manual:Collapsible_elements/Demo/Advanced]] are supported with the following
+ * exceptions
+ * - Custom collapsible 4 (table-row)
+ * -- CSS selector would be too complicated
+ * - Collapsible div nested in collapsed div
+ * -- Given it's not easy to identify the collapsed content via CSS, text will be shown until
+ *    JavaScript has loaded
+ * - "Combination example"
+ * -- At a later time, we may want to support the use of of `attr`, but given the code
+ *    complexity we will not for the time being (see https://davidwalsh.name/css-content-attr)
+ */
+
+// This selector is used frequently in the code to indicate that the JavaScript has successfully completed
+// it's execution and pseudo elements can be disabled. For readability and maintainability it is separated
+// as a LESS variable.
+@exclude: ~'.mw-made-collapsible';
+
+.client-js {
+
+       ol.mw-collapsible:before,
+       ul.mw-collapsible:before,
+       .mw-collapsible-toggle-li {
+               /*
+               Rather than inherit any margins from the the general li selector - make sure this is explicit
+               to avoid reflows
+               */
+               display: list-item;
+               list-style: none;
+               margin-bottom: 0.1em;
+       }
+
+       // Reset when mw-collapsible-toggle-li is rendered
+       ol.mw-made-collapsible:before,
+       ul.mw-made-collapsible:before {
+               display: none;
+       }
+
+       ol.mw-collapsible:not( @{exclude} ):before,
+       ul.mw-collapsible:not( @{exclude} ):before,
+       // Where the tbody or thead is the first child of the collapsible table
+       table.mw-collapsible:not( @{exclude} ) :first-child tr:first-child th:last-child:before,
+       table.mw-collapsible:not( @{exclude} ) > caption:first-child:after {
+               content: '[@{msg-collapsible-collapse}]';
+       }
+
+       td.mw-collapsed:not( @{exclude} ):before,
+       table.mw-collapsed:not( @{exclude} ) :first-child tr:first-child th:last-child:before,
+       table.mw-collapsed:not( @{exclude} ) > caption:first-child:after,
+       div.mw-collapsed:not( @{exclude} ):before {
+               content: '[@{msg-collapsible-expand}]';
+       }
+
+       // Any element with id beginning `mw-customcollapsible` will have special treatment
+       .mw-collapsible[ id^='mw-customcollapsible' ] th:before,
+       .mw-collapsible[ id^='mw-customcollapsible' ]:before {
+               content: none !important; // stylelint-disable-line declaration-no-important
+       }
+
+       // Special case for table where first child is caption element
+       table.mw-collapsible:not( @{exclude} ) > caption:first-child:after {
+               float: none;
+               display: block;
+       }
+
+       // Use the exclude selector to ensure animations do not break
+       .mw-collapsed:not( @{exclude} ) {
+               // Avoid FOUC/reflows on collapsed elements by making sure they are opened by default (T42812)
+               > p,
+               > table,
+               // Manual:Collapsible_elements/Demo/Simple#Collapsed_by_default
+               > thead + tbody,
+               tr:not( :first-child ),
+               .mw-collapsible-content {
+                       display: none;
+               }
+       }
+}
+
+/* Align the toggle based on the direction of the content language */
+/* @noflip */
+.mw-content-ltr,
+.mw-content-rtl .mw-content-ltr {
+       .mw-collapsible:not( @{exclude} ) th:before,
+       .mw-collapsible:not( @{exclude} ):before,
+       .mw-collapsible-toggle {
+               float: right;
+       }
+}
+
+/* @noflip */
+.mw-content-rtl,
+.mw-content-ltr .mw-content-rtl {
+       .mw-collapsible:not( @{exclude} ) th:before,
+       .mw-collapsible:not( @{exclude} ):before,
+       .mw-collapsible-toggle {
+               float: left;
+       }
+}
+
+/* list-items go as wide as their parent element, don't float them inside list items */
+li,
+.mw-content-ltr li,
+.mw-content-rtl li,
+.mw-content-ltr .mw-content-rtl li,
+.mw-content-rtl .mw-content-ltr li {
+       .mw-collapsible-toggle {
+               float: none;
+       }
+}
+
+// special treatment for list items to match above
+// !important necessary to override overly-specific float left and right above.
+ol.mw-collapsible:not( @{exclude} ):before,
+ul.mw-collapsible:not( @{exclude} ):before {
+       float: none !important; // stylelint-disable-line declaration-no-important
+}
index ce24b0d..3bea471 100644 (file)
@@ -8,7 +8,10 @@ table.jquery-tablesorter {
                cursor: pointer;
                background-repeat: no-repeat;
                background-position: center right;
-               padding-right: 21px;
+               // Note: To avoid reflows, a padding is set in
+               // the jquery.tableSorter.styles module as a render blocking style.
+               // Please do not add any CSS rules here that impact the positioning of the element
+               // e.g. padding, margin, position or float.
        }
 
        th.headerSortUp {
diff --git a/resources/src/jquery/jquery.tablesorter.styles.less b/resources/src/jquery/jquery.tablesorter.styles.less
new file mode 100644 (file)
index 0000000..bd6b5dd
--- /dev/null
@@ -0,0 +1,6 @@
+.client-js {
+       // Reserve space for table sortable controls
+       table.sortable th {
+               padding-right: 21px;
+       }
+}
diff --git a/resources/src/mediawiki.libs.jpegmeta/export.js b/resources/src/mediawiki.libs.jpegmeta/export.js
new file mode 100644 (file)
index 0000000..e8913aa
--- /dev/null
@@ -0,0 +1,12 @@
+/* global JpegMeta */
+( function ( mw ) {
+
+       // Export as module
+       module.exports = function ( fileReaderResult, fileName ) {
+               return new JpegMeta.JpegFile( fileReaderResult, fileName );
+       };
+
+       // Back-compat: Also expose via mw.lib
+       // @deprecated since 1.31
+       mw.log.deprecate( mw.libs, 'jpegmeta', module.exports );
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.libs.jpegmeta/jpegmeta.js b/resources/src/mediawiki.libs.jpegmeta/jpegmeta.js
new file mode 100644 (file)
index 0000000..ed85914
--- /dev/null
@@ -0,0 +1,731 @@
+/**
+ * This is JsJpegMeta v1.0
+ * From: https://code.google.com/p/jsjpegmeta/downloads/list
+ * From: https://github.com/bennoleslie/jsjpegmeta/blob/v1.0.0/jpegmeta.js
+ *
+ * Ported to MediaWiki ResourceLoader by Bryan Tong Minh
+ * Changes:
+ * - Add closure.
+ * - Add this.JpegMeta assignment to expose it as global.
+ */
+
+( function () {
+       /*
+       Copyright (c) 2009 Ben Leslie
+       
+       Permission is hereby granted, free of charge, to any person obtaining a copy
+       of this software and associated documentation files (the "Software"), to deal
+       in the Software without restriction, including without limitation the rights
+       to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+       copies of the Software, and to permit persons to whom the Software is
+       furnished to do so, subject to the following conditions:
+       
+       The above copyright notice and this permission notice shall be included in
+       all copies or substantial portions of the Software.
+       
+       THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+       IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+       FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+       AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+       LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+       OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+       THE SOFTWARE.
+       */
+       
+       /*
+        This JavaScript library is used to parse meta-data from files 
+        with mime-type image/jpeg.
+       
+        Include it with something like:
+       
+          <script type="text/javascript" src="jpegmeta.js"></script>
+       
+        This adds a single 'module' object called 'JpegMeta' to the global
+        namespace.
+       
+        Public Functions
+        ----------------
+        JpegMeta.parseNum - parse unsigned integers from binary data
+        JpegMeta.parseSnum - parse signed integers from binary data
+       
+        Public Classes
+        --------------
+        JpegMeta.Rational - A rational number class
+        JpegMeta.JfifSegment
+        JpegMeta.ExifSegment
+        JpegMeta.JpegFile - Primary class for Javascript parsing
+       */
+
+       var JpegMeta = {};
+       // MediaWiki: Expose as global
+       this.JpegMeta = JpegMeta;
+       
+       /* 
+          parse an unsigned number of size bytes at offset in some binary string data.
+          If endian
+          is "<" parse the data as little endian, if endian
+          is ">" parse as big-endian.
+       */
+       JpegMeta.parseNum = function parseNum(endian, data, offset, size) {
+           var i;
+           var ret;
+           var big_endian = (endian === ">");
+           if (offset === undefined) offset = 0;
+           if (size === undefined) size = data.length - offset;
+           for (big_endian ? i = offset : i = offset + size - 1; 
+                big_endian ? i < offset + size : i >= offset; 
+                big_endian ? i++ : i--) {
+               ret <<= 8;
+               ret += data.charCodeAt(i);
+           }
+           return ret;
+       };
+       
+       /* 
+          parse an signed number of size bytes at offset in some binary string data.
+          If endian
+          is "<" parse the data as little endian, if endian
+          is ">" parse as big-endian.
+       */
+       JpegMeta.parseSnum = function parseSnum(endian, data, offset, size) {
+           var i;
+           var ret;
+           var neg;
+           var big_endian = (endian === ">");
+           if (offset === undefined) offset = 0;
+           if (size === undefined) size = data.length - offset;
+           for (big_endian ? i = offset : i = offset + size - 1; 
+                big_endian ? i < offset + size : i >= offset; 
+                big_endian ? i++ : i--) {
+               if (neg === undefined) {
+                   /* Negative if top bit is set */
+                   neg = (data.charCodeAt(i) & 0x80) === 0x80;
+               }
+               ret <<= 8;
+               /* If it is negative we invert the bits */
+               ret += neg ? ~data.charCodeAt(i) & 0xff: data.charCodeAt(i);
+           }
+           if (neg) {
+               /* If it is negative we do two's complement */
+               ret += 1;
+               ret *= -1;
+           }
+           return ret;
+       };
+       
+       /* Rational number class */
+       JpegMeta.Rational = function Rational(num, den)
+       {
+           this.num = num;
+           this.den = den || 1;
+           return this;
+       };
+       
+       /* Rational number methods */
+       JpegMeta.Rational.prototype.toString = function toString() {
+           if (this.num === 0) {
+               return "" + this.num;
+           }
+           if (this.den === 1) {
+               return "" + this.num;
+           }
+           if (this.num === 1) {
+               return this.num + " / " + this.den;
+           }
+           return this.num / this.den; // + "/" + this.den;
+       };
+       
+       JpegMeta.Rational.prototype.asFloat = function asFloat() {
+           return this.num / this.den;
+       };
+       
+       /* MetaGroup class */
+       JpegMeta.MetaGroup = function MetaGroup(fieldName, description) {
+           this.fieldName = fieldName;
+           this.description = description;
+           this.metaProps = {};
+           return this;
+       };
+       
+       JpegMeta.MetaGroup.prototype._addProperty = function _addProperty(fieldName, description, value) {
+           var property = new JpegMeta.MetaProp(fieldName, description, value);
+           this[property.fieldName] = property;
+           this.metaProps[property.fieldName] = property;
+       };
+       
+       JpegMeta.MetaGroup.prototype.toString = function toString() {
+           return "[MetaGroup " + this.description + "]";
+       };
+
+       /* MetaProp class */
+       JpegMeta.MetaProp = function MetaProp(fieldName, description, value) {
+           this.fieldName = fieldName;
+           this.description = description;
+           this.value = value;
+           return this;
+       };
+       
+       JpegMeta.MetaProp.prototype.toString = function toString() {
+           return "" + this.value;
+       };
+
+       /* JpegFile class */
+       JpegMeta.JpegFile = function JpegFile(binary_data, filename) {
+           /* Change this to EOI if we want to parse. */
+           var break_segment = this._SOS;
+           
+           this.metaGroups = {};
+           this._binary_data = binary_data;
+           this.filename = filename;
+           
+           /* Go through and parse. */
+           var pos = 0;
+           var pos_start_of_segment = 0;
+           var delim;
+           var mark;
+           var _mark;
+           var segsize;
+           var headersize;
+           var mark_code;
+           var mark_fn;
+       
+           /* Check to see if this looks like a JPEG file */
+           if (this._binary_data.slice(0, 2) !== this._SOI_MARKER) {
+               throw new Error("Doesn't look like a JPEG file. First two bytes are " + 
+                               this._binary_data.charCodeAt(0) + "," + 
+                               this._binary_data.charCodeAt(1) + ".");
+           }
+           
+           pos += 2;
+           
+           while (pos < this._binary_data.length) {
+               delim = this._binary_data.charCodeAt(pos++);
+               mark = this._binary_data.charCodeAt(pos++);
+               
+               pos_start_of_segment = pos;
+               
+               if (delim != this._DELIM) {
+                   break;
+               }
+               
+               if (mark === break_segment) {
+                   break;
+               }
+               
+               headersize = JpegMeta.parseNum(">", this._binary_data, pos, 2);
+               
+               /* Find the end */
+               pos += headersize;
+               while (pos < this._binary_data.length) {
+                   delim = this._binary_data.charCodeAt(pos++);
+                   if (delim == this._DELIM) {
+                       _mark = this._binary_data.charCodeAt(pos++);
+                       if (_mark != 0x0) {
+                           pos -= 2;
+                           break;
+                       }
+                   }
+               }
+               
+               segsize = pos - pos_start_of_segment;
+               
+               if (this._markers[mark]) {
+                   mark_code = this._markers[mark][0];
+                   mark_fn = this._markers[mark][1];
+               } else {
+                   mark_code = "UNKN";
+                   mark_fn = undefined;
+               }
+               
+               if (mark_fn) {
+                   this[mark_fn](mark, pos_start_of_segment + 2);
+               }
+               
+           }
+           
+           if (this.general === undefined) {
+               throw Error("Invalid JPEG file.");
+           }
+           
+           return this;
+       };
+       
+       this.JpegMeta.JpegFile.prototype.toString = function () {
+           return "[JpegFile " + this.filename + " " + 
+               this.general.type + " " + 
+               this.general.pixelWidth + "x" + 
+               this.general.pixelHeight +
+               " Depth: " + this.general.depth + "]";
+       };
+       
+       /* Some useful constants */
+       this.JpegMeta.JpegFile.prototype._SOI_MARKER = '\xff\xd8';
+       this.JpegMeta.JpegFile.prototype._DELIM = 0xff;
+       this.JpegMeta.JpegFile.prototype._EOI = 0xd9;
+       this.JpegMeta.JpegFile.prototype._SOS = 0xda;
+       
+       this.JpegMeta.JpegFile.prototype._sofHandler = function _sofHandler (mark, pos) {
+           if (this.general !== undefined) {
+               throw Error("Unexpected multiple-frame image");
+           }
+       
+           this._addMetaGroup("general", "General");
+           this.general._addProperty("depth", "Depth", JpegMeta.parseNum(">", this._binary_data, pos, 1));
+           this.general._addProperty("pixelHeight", "Pixel Height", JpegMeta.parseNum(">", this._binary_data, pos + 1, 2));
+           this.general._addProperty("pixelWidth", "Pixel Width",JpegMeta.parseNum(">", this._binary_data, pos + 3, 2));
+           this.general._addProperty("type", "Type", this._markers[mark][2]);
+       };
+       
+       /* JFIF idents */
+       this.JpegMeta.JpegFile.prototype._JFIF_IDENT = "JFIF\x00";
+       this.JpegMeta.JpegFile.prototype._JFXX_IDENT = "JFXX\x00";
+       
+       /* Exif idents */
+       this.JpegMeta.JpegFile.prototype._EXIF_IDENT = "Exif\x00";
+       
+       /* TIFF types */
+       this.JpegMeta.JpegFile.prototype._types = {
+           /* The format is identifier : ["type name", type_size_in_bytes ] */
+           1 : ["BYTE", 1],
+           2 : ["ASCII", 1],
+           3 : ["SHORT", 2],
+           4 : ["LONG", 4],
+           5 : ["RATIONAL", 8],
+           6 : ["SBYTE", 1],
+           7 : ["UNDEFINED", 1],
+           8 : ["SSHORT", 2],
+           9 : ["SLONG", 4],
+           10 : ["SRATIONAL", 8],
+           11 : ["FLOAT", 4],
+           12 : ["DOUBLE", 8]
+       };
+       
+       this.JpegMeta.JpegFile.prototype._tifftags = {
+           /* A. Tags relating to image data structure */
+           256 : ["Image width", "ImageWidth"],
+           257 : ["Image height", "ImageLength"],
+           258 : ["Number of bits per component", "BitsPerSample"],
+           259 : ["Compression scheme", "Compression", 
+                  {1 : "uncompressed", 6 : "JPEG compression" }],
+           262 : ["Pixel composition", "PhotmetricInerpretation",
+                  {2 : "RGB", 6 : "YCbCr"}],
+           274 : ["Orientation of image", "Orientation",
+                  /* FIXME: Check the mirror-image / reverse encoding and rotation */
+                  {1 : "Normal", 2 : "Reverse?", 
+                   3 : "Upside-down", 4 : "Upside-down Reverse",
+                   5 : "90 degree CW", 6 : "90 degree CW reverse",
+                   7 : "90 degree CCW", 8 : "90 degree CCW reverse"}],
+           277 : ["Number of components", "SamplesPerPixel"],
+           284 : ["Image data arrangement", "PlanarConfiguration",
+                  {1 : "chunky format", 2 : "planar format"}],
+           530 : ["Subsampling ratio of Y to C", "YCbCrSubSampling"],
+           531 : ["Y and C positioning", "YCbCrPositioning",
+                  {1 : "centered", 2 : "co-sited"}],
+           282 : ["X Resolution", "XResolution"],
+           283 : ["Y Resolution", "YResolution"],
+           296 : ["Resolution Unit", "ResolutionUnit",
+                  {2 : "inches", 3 : "centimeters"}],
+           /* B. Tags realting to recording offset */
+           273 : ["Image data location", "StripOffsets"],
+           278 : ["Number of rows per strip", "RowsPerStrip"],
+           279 : ["Bytes per compressed strip", "StripByteCounts"],
+           513 : ["Offset to JPEG SOI", "JPEGInterchangeFormat"],
+           514 : ["Bytes of JPEG Data", "JPEGInterchangeFormatLength"],
+           /* C. Tags relating to image data characteristics */
+           301 : ["Transfer function", "TransferFunction"],
+           318 : ["White point chromaticity", "WhitePoint"],
+           319 : ["Chromaticities of primaries", "PrimaryChromaticities"],
+           529 : ["Color space transformation matrix coefficients", "YCbCrCoefficients"],
+           532 : ["Pair of black and white reference values", "ReferenceBlackWhite"],
+           /* D. Other tags */
+           306 : ["Date and time", "DateTime"],
+           270 : ["Image title", "ImageDescription"],
+           271 : ["Make", "Make"],
+           272 : ["Model", "Model"],
+           305 : ["Software", "Software"],
+           315 : ["Person who created the image", "Artist"],
+           316 : ["Host Computer", "HostComputer"],
+           33432 : ["Copyright holder", "Copyright"],
+           
+           34665 : ["Exif tag", "ExifIfdPointer"],
+           34853 : ["GPS tag", "GPSInfoIfdPointer"]
+       };
+       
+       this.JpegMeta.JpegFile.prototype._exiftags = {
+           /* Tag Support Levels (2) - 0th IFX Exif Private Tags */
+           /* A. Tags Relating to Version */
+           36864 : ["Exif Version", "ExifVersion"],
+           40960 : ["FlashPix Version", "FlashpixVersion"],
+           
+           /* B. Tag Relating to Image Data Characteristics */
+           40961 : ["Color Space", "ColorSpace"],
+           
+           /* C. Tags Relating to Image Configuration */
+           37121 : ["Meaning of each component", "ComponentsConfiguration"],
+           37122 : ["Compressed Bits Per Pixel", "CompressedBitsPerPixel"],
+           40962 : ["Pixel X Dimension", "PixelXDimension"],
+           40963 : ["Pixel Y Dimension", "PixelYDimension"],
+           
+           /* D. Tags Relating to User Information */
+           37500 : ["Manufacturer notes", "MakerNote"],
+           37510 : ["User comments", "UserComment"],
+           
+           /* E. Tag Relating to Related File Information */
+           40964 : ["Related audio file", "RelatedSoundFile"],
+           
+           /* F. Tags Relating to Date and Time */
+           36867 : ["Date Time Original", "DateTimeOriginal"],
+           36868 : ["Date Time Digitized", "DateTimeDigitized"],
+           37520 : ["DateTime subseconds", "SubSecTime"],
+           37521 : ["DateTimeOriginal subseconds", "SubSecTimeOriginal"],
+           37522 : ["DateTimeDigitized subseconds", "SubSecTimeDigitized"],
+           
+           /* G. Tags Relating to Picture-Taking Conditions */
+           33434 : ["Exposure time", "ExposureTime"],
+           33437 : ["FNumber", "FNumber"],
+           34850 : ["Exposure program", "ExposureProgram"],
+           34852 : ["Spectral sensitivity", "SpectralSensitivity"],
+           34855 : ["ISO Speed Ratings", "ISOSpeedRatings"],
+           34856 : ["Optoelectric coefficient", "OECF"],
+           37377 : ["Shutter Speed",  "ShutterSpeedValue"],
+           37378 : ["Aperture Value", "ApertureValue"],
+           37379 : ["Brightness", "BrightnessValue"],
+           37380 : ["Exposure Bias Value", "ExposureBiasValue"],
+           37381 : ["Max Aperture Value", "MaxApertureValue"],
+           37382 : ["Subject Distance", "SubjectDistance"],
+           37383 : ["Metering Mode", "MeteringMode"],
+           37384 : ["Light Source", "LightSource"],
+           37385 : ["Flash", "Flash"],
+           37386 : ["Focal Length", "FocalLength"],
+           37396 : ["Subject Area", "SubjectArea"],
+           41483 : ["Flash Energy", "FlashEnergy"],
+           41484 : ["Spatial Frequency Response", "SpatialFrequencyResponse"],
+           41486 : ["Focal Plane X Resolution", "FocalPlaneXResolution"],
+           41487 : ["Focal Plane Y Resolution", "FocalPlaneYResolution"],
+           41488 : ["Focal Plane Resolution Unit", "FocalPlaneResolutionUnit"],
+           41492 : ["Subject Location", "SubjectLocation"],
+           41493 : ["Exposure Index", "ExposureIndex"],
+           41495 : ["Sensing Method", "SensingMethod"],
+           41728 : ["File Source", "FileSource"],
+           41729 : ["Scene Type", "SceneType"],
+           41730 : ["CFA Pattern", "CFAPattern"],
+           41985 : ["Custom Rendered", "CustomRendered"],
+           41986 : ["Exposure Mode", "Exposure Mode"],
+           41987 : ["White Balance", "WhiteBalance"],
+           41988 : ["Digital Zoom Ratio", "DigitalZoomRatio"],
+           41990 : ["Scene Capture Type", "SceneCaptureType"],
+           41991 : ["Gain Control", "GainControl"],
+           41992 : ["Contrast", "Contrast"],
+           41993 : ["Saturation", "Saturation"],
+           41994 : ["Sharpness", "Sharpness"],
+           41995 : ["Device settings description", "DeviceSettingDescription"],
+           41996 : ["Subject distance range", "SubjectDistanceRange"],
+           
+           /* H. Other Tags */
+           42016 : ["Unique image ID", "ImageUniqueID"],
+           
+           40965 : ["Interoperability tag", "InteroperabilityIFDPointer"]
+       };
+       
+       this.JpegMeta.JpegFile.prototype._gpstags = {
+           /* A. Tags Relating to GPS */
+           0 : ["GPS tag version", "GPSVersionID"],
+           1 : ["North or South Latitude", "GPSLatitudeRef"],
+           2 : ["Latitude", "GPSLatitude"],
+           3 : ["East or West Longitude", "GPSLongitudeRef"],
+           4 : ["Longitude", "GPSLongitude"],
+           5 : ["Altitude reference", "GPSAltitudeRef"],
+           6 : ["Altitude", "GPSAltitude"],
+           7 : ["GPS time (atomic clock)", "GPSTimeStamp"],
+           8 : ["GPS satellites usedd for measurement", "GPSSatellites"],
+           9 : ["GPS receiver status", "GPSStatus"],
+           10 : ["GPS mesaurement mode", "GPSMeasureMode"],
+           11 : ["Measurement precision", "GPSDOP"],
+           12 : ["Speed unit", "GPSSpeedRef"],
+           13 : ["Speed of GPS receiver", "GPSSpeed"],
+           14 : ["Reference for direction of movement", "GPSTrackRef"],
+           15 : ["Direction of movement", "GPSTrack"],
+           16 : ["Reference for direction of image", "GPSImgDirectionRef"],
+           17 : ["Direction of image", "GPSImgDirection"],
+           18 : ["Geodetic survey data used", "GPSMapDatum"],
+           19 : ["Reference for latitude of destination", "GPSDestLatitudeRef"],
+           20 : ["Latitude of destination", "GPSDestLatitude"],
+           21 : ["Reference for longitude of destination", "GPSDestLongitudeRef"],
+           22 : ["Longitude of destination", "GPSDestLongitude"],
+           23 : ["Reference for bearing of destination", "GPSDestBearingRef"],
+           24 : ["Bearing of destination", "GPSDestBearing"],
+           25 : ["Reference for distance to destination", "GPSDestDistanceRef"],
+           26 : ["Distance to destination", "GPSDestDistance"],
+           27 : ["Name of GPS processing method", "GPSProcessingMethod"],
+           28 : ["Name of GPS area", "GPSAreaInformation"],
+           29 : ["GPS Date", "GPSDateStamp"],
+           30 : ["GPS differential correction", "GPSDifferential"]
+       };
+
+       this.JpegMeta.JpegFile.prototype._markers = {
+           /* Start Of Frame markers, non-differential, Huffman coding */
+           0xc0: ["SOF0", "_sofHandler", "Baseline DCT"],
+           0xc1: ["SOF1", "_sofHandler", "Extended sequential DCT"],
+           0xc2: ["SOF2", "_sofHandler", "Progressive DCT"],
+           0xc3: ["SOF3", "_sofHandler", "Lossless (sequential)"],
+           
+           /* Start Of Frame markers, differential, Huffman coding */
+           0xc5: ["SOF5", "_sofHandler", "Differential sequential DCT"],
+           0xc6: ["SOF6", "_sofHandler", "Differential progressive DCT"],
+           0xc7: ["SOF7", "_sofHandler", "Differential lossless (sequential)"],
+           
+           /* Start Of Frame markers, non-differential, arithmetic coding */
+           0xc8: ["JPG", null, "Reserved for JPEG extensions"],
+           0xc9: ["SOF9", "_sofHandler", "Extended sequential DCT"],
+           0xca: ["SOF10", "_sofHandler", "Progressive DCT"],
+           0xcb: ["SOF11", "_sofHandler", "Lossless (sequential)"],
+           
+           /* Start Of Frame markers, differential, arithmetic coding */
+           0xcd: ["SOF13", "_sofHandler", "Differential sequential DCT"],
+           0xce: ["SOF14", "_sofHandler", "Differential progressive DCT"],
+           0xcf: ["SOF15", "_sofHandler", "Differential lossless (sequential)"],
+           
+           /* Huffman table specification */
+           0xc4: ["DHT", null, "Define Huffman table(s)"],
+           0xcc: ["DAC", null, "Define arithmetic coding conditioning(s)"],
+           
+           /* Restart interval termination" */
+           0xd0: ["RST0", null, "Restart with modulo 8 count “0”"],
+           0xd1: ["RST1", null, "Restart with modulo 8 count “1”"],
+           0xd2: ["RST2", null, "Restart with modulo 8 count “2”"],
+           0xd3: ["RST3", null, "Restart with modulo 8 count “3”"],
+           0xd4: ["RST4", null, "Restart with modulo 8 count “4”"],
+           0xd5: ["RST5", null, "Restart with modulo 8 count “5”"],
+           0xd6: ["RST6", null, "Restart with modulo 8 count “6”"],
+           0xd7: ["RST7", null, "Restart with modulo 8 count “7”"],
+           
+           /* Other markers */
+           0xd8: ["SOI", null, "Start of image"],
+           0xd9: ["EOI", null, "End of image"],
+           0xda: ["SOS", null, "Start of scan"],
+           0xdb: ["DQT", null, "Define quantization table(s)"],
+           0xdc: ["DNL", null, "Define number of lines"],
+           0xdd: ["DRI", null, "Define restart interval"],
+           0xde: ["DHP", null, "Define hierarchical progression"],
+           0xdf: ["EXP", null, "Expand reference component(s)"],
+           0xe0: ["APP0", "_app0Handler", "Reserved for application segments"],
+           0xe1: ["APP1", "_app1Handler"],
+           0xe2: ["APP2", null],
+           0xe3: ["APP3", null],
+           0xe4: ["APP4", null],
+           0xe5: ["APP5", null],
+           0xe6: ["APP6", null],
+           0xe7: ["APP7", null],
+           0xe8: ["APP8", null],
+           0xe9: ["APP9", null],
+           0xea: ["APP10", null],
+           0xeb: ["APP11", null],
+           0xec: ["APP12", null],
+           0xed: ["APP13", null],
+           0xee: ["APP14", null],
+           0xef: ["APP15", null],
+           0xf0: ["JPG0", null], /* Reserved for JPEG extensions */
+           0xf1: ["JPG1", null],
+           0xf2: ["JPG2", null],
+           0xf3: ["JPG3", null],
+           0xf4: ["JPG4", null],
+           0xf5: ["JPG5", null],
+           0xf6: ["JPG6", null],
+           0xf7: ["JPG7", null],
+           0xf8: ["JPG8", null],
+           0xf9: ["JPG9", null],
+           0xfa: ["JPG10", null],
+           0xfb: ["JPG11", null],
+           0xfc: ["JPG12", null],
+           0xfd: ["JPG13", null],
+           0xfe: ["COM", null], /* Comment */
+           
+           /* Reserved markers */
+           0x01: ["JPG13", null] /* For temporary private use in arithmetic coding */
+           /* 02 -> bf are reserverd */
+       };
+
+       /* Private methods */
+       this.JpegMeta.JpegFile.prototype._addMetaGroup = function _addMetaGroup(name, description) {
+           var group = new JpegMeta.MetaGroup(name, description);
+           this[group.fieldName] = group;
+           this.metaGroups[group.fieldName] = group;
+           return group;
+       };
+
+       this.JpegMeta.JpegFile.prototype._parseIfd = function _parseIfd(endian, _binary_data, base, ifd_offset, tags, name, description) {
+           var num_fields = JpegMeta.parseNum(endian, _binary_data, base + ifd_offset, 2);
+           /* Per tag variables */
+           var i, j;
+           var tag_base;
+           var tag_field;
+           var type, type_field, type_size;
+           var num_values;
+           var value_offset;
+           var value;
+           var _val;
+           var num;
+           var den;
+           
+           var group;
+           
+           group = this._addMetaGroup(name, description);
+       
+           for (var i = 0; i < num_fields; i++) {
+               /* parse the field */
+               tag_base = base + ifd_offset + 2 + (i * 12);
+               tag_field = JpegMeta.parseNum(endian, _binary_data, tag_base, 2);
+               type_field = JpegMeta.parseNum(endian, _binary_data, tag_base + 2, 2);
+               num_values = JpegMeta.parseNum(endian, _binary_data, tag_base + 4, 4);
+               value_offset = JpegMeta.parseNum(endian, _binary_data, tag_base + 8, 4);
+               if (this._types[type_field] === undefined) {
+                   continue;
+               }
+               type = this._types[type_field][0];
+               type_size = this._types[type_field][1];
+               
+               if (type_size * num_values <= 4) {
+                   /* Data is in-line */
+                   value_offset = tag_base + 8;
+               } else {
+                   value_offset = base + value_offset;
+               }
+               
+               /* Read the value */
+               if (type == "UNDEFINED") {
+                   value = _binary_data.slice(value_offset, value_offset + num_values);
+               } else if (type == "ASCII") {
+                   value = _binary_data.slice(value_offset, value_offset + num_values);
+                   value = value.split('\x00')[0];
+                   /* strip trail nul */
+               } else {
+                   value = new Array();
+                   for (j = 0; j < num_values; j++, value_offset += type_size) {
+                       if (type == "BYTE" || type == "SHORT" || type == "LONG") {
+                           value.push(JpegMeta.parseNum(endian, _binary_data, value_offset, type_size));
+                       }
+                       if (type == "SBYTE" || type == "SSHORT" || type == "SLONG") {
+                           value.push(JpegMeta.parseSnum(endian, _binary_data, value_offset, type_size));
+                       }
+                       if (type == "RATIONAL") {
+                           num = JpegMeta.parseNum(endian, _binary_data, value_offset, 4);
+                           den = JpegMeta.parseNum(endian, _binary_data, value_offset + 4, 4);
+                           value.push(new JpegMeta.Rational(num, den));
+                       }
+                       if (type == "SRATIONAL") {
+                           num = JpegMeta.parseSnum(endian, _binary_data, value_offset, 4);
+                           den = JpegMeta.parseSnum(endian, _binary_data, value_offset + 4, 4);
+                           value.push(new JpegMeta.Rational(num, den));
+                       }
+                       value.push();
+                   }
+                   if (num_values === 1) {
+                       value = value[0];
+                   }
+               }
+               if (tags[tag_field] !== undefined) {
+                       group._addProperty(tags[tag_field][1], tags[tag_field][0], value);
+               }
+           }
+       };
+
+       this.JpegMeta.JpegFile.prototype._jfifHandler = function _jfifHandler(mark, pos) {
+           if (this.jfif !== undefined) {
+               throw Error("Multiple JFIF segments found");
+           }
+           this._addMetaGroup("jfif", "JFIF");
+           this.jfif._addProperty("version_major", "Version Major", this._binary_data.charCodeAt(pos + 5));
+           this.jfif._addProperty("version_minor", "Version Minor", this._binary_data.charCodeAt(pos + 6));
+           this.jfif._addProperty("version", "JFIF Version", this.jfif.version_major.value + "." + this.jfif.version_minor.value);
+           this.jfif._addProperty("units", "Density Unit", this._binary_data.charCodeAt(pos + 7));
+           this.jfif._addProperty("Xdensity", "X density", JpegMeta.parseNum(">", this._binary_data, pos + 8, 2));
+           this.jfif._addProperty("Ydensity", "Y Density", JpegMeta.parseNum(">", this._binary_data, pos + 10, 2));
+           this.jfif._addProperty("Xthumbnail", "X Thumbnail", JpegMeta.parseNum(">", this._binary_data, pos + 12, 1));
+           this.jfif._addProperty("Ythumbnail", "Y Thumbnail", JpegMeta.parseNum(">", this._binary_data, pos + 13, 1));
+       };
+
+       /* Handle app0 segments */
+       this.JpegMeta.JpegFile.prototype._app0Handler = function app0Handler(mark, pos) {
+           var ident = this._binary_data.slice(pos, pos + 5);
+           if (ident == this._JFIF_IDENT) {
+               this._jfifHandler(mark, pos);
+           } else if (ident == this._JFXX_IDENT) {
+               /* Don't handle JFXX Ident yet */
+           } else {
+               /* Don't know about other idents */
+           }
+       };
+
+       /* Handle app1 segments */
+       this.JpegMeta.JpegFile.prototype._app1Handler = function _app1Handler(mark, pos) {
+           var ident = this._binary_data.slice(pos, pos + 5);
+           if (ident == this._EXIF_IDENT) {
+               this._exifHandler(mark, pos + 6);
+           } else {
+               /* Don't know about other idents */
+           }
+       };
+
+       /* Handle exif segments */
+       JpegMeta.JpegFile.prototype._exifHandler = function _exifHandler(mark, pos) {
+           if (this.exif !== undefined) {
+               throw new Error("Multiple JFIF segments found");
+           }
+           
+           /* Parse this TIFF header */
+           var endian;
+           var magic_field;
+           var ifd_offset;
+           var primary_ifd, exif_ifd, gps_ifd;
+           var endian_field = this._binary_data.slice(pos, pos + 2);
+           
+           /* Trivia: This 'I' is for Intel, the 'M' is for Motorola */
+           if (endian_field === "II") {
+               endian = "<";
+           } else if (endian_field === "MM") {
+               endian = ">";
+           } else {
+               throw new Error("Malformed TIFF meta-data. Unknown endianess: " + endian_field);
+           }
+           
+           magic_field = JpegMeta.parseNum(endian, this._binary_data, pos + 2, 2);
+           
+           if (magic_field !== 42) {
+               throw new Error("Malformed TIFF meta-data. Bad magic: " + magic_field);
+           }
+           
+           ifd_offset = JpegMeta.parseNum(endian, this._binary_data, pos + 4, 4);
+           
+           /* Parse 0th IFD */
+           this._parseIfd(endian, this._binary_data, pos, ifd_offset, this._tifftags, "tiff", "TIFF");
+           
+           if (this.tiff.ExifIfdPointer) {
+               this._parseIfd(endian, this._binary_data, pos, this.tiff.ExifIfdPointer.value, this._exiftags, "exif", "Exif");
+           }
+           
+           if (this.tiff.GPSInfoIfdPointer) {
+               this._parseIfd(endian, this._binary_data, pos, this.tiff.GPSInfoIfdPointer.value, this._gpstags, "gps", "GPS");
+               if (this.gps.GPSLatitude) {
+                   var latitude;
+                   latitude = this.gps.GPSLatitude.value[0].asFloat() + 
+                       (1 / 60) * this.gps.GPSLatitude.value[1].asFloat() + 
+                       (1 / 3600) * this.gps.GPSLatitude.value[2].asFloat();
+                   if (this.gps.GPSLatitudeRef.value === "S") {
+                       latitude = -latitude;
+                   }
+                   this.gps._addProperty("latitude", "Dec. Latitude", latitude);
+               }
+               if (this.gps.GPSLongitude) {
+                   var longitude;
+                   longitude = this.gps.GPSLongitude.value[0].asFloat() + 
+                       (1 / 60) * this.gps.GPSLongitude.value[1].asFloat() + 
+                       (1 / 3600) * this.gps.GPSLongitude.value[2].asFloat();
+                   if (this.gps.GPSLongitudeRef.value === "W") {
+                       longitude = -longitude;
+                   }
+                   this.gps._addProperty("longitude", "Dec. Longitude", longitude);
+               }
+           }
+       };
+
+}() );
diff --git a/resources/src/mediawiki.libs.pluralruleparser/export.js b/resources/src/mediawiki.libs.pluralruleparser/export.js
new file mode 100644 (file)
index 0000000..28449d3
--- /dev/null
@@ -0,0 +1,5 @@
+// Expose via module.exports
+module.exports = window.pluralRuleParser;
+
+// Back-compat: Also expose via mw.lib
+mediaWiki.libs.pluralRuleParser = window.pluralRuleParser;
diff --git a/resources/src/mediawiki.libs/CLDRPluralRuleParser.js b/resources/src/mediawiki.libs/CLDRPluralRuleParser.js
deleted file mode 100644 (file)
index 549a9ab..0000000
+++ /dev/null
@@ -1,596 +0,0 @@
-/* This is CLDRPluralRuleParser v1.1.3, ported to MediaWiki ResourceLoader */
-
-/**
-* CLDRPluralRuleParser.js
-* A parser engine for CLDR plural rules.
-*
-* Copyright 2012-2014 Santhosh Thottingal and other contributors
-* Released under the MIT license
-* http://opensource.org/licenses/MIT
-*
-* @source https://github.com/santhoshtr/CLDRPluralRuleParser
-* @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
-* @author Timo Tijhof
-* @author Amir Aharoni
-*/
-
-( function ( mw ) {
-/**
- * Evaluates a plural rule in CLDR syntax for a number
- * @param {string} rule
- * @param {integer} number
- * @return {boolean} true if evaluation passed, false if evaluation failed.
- */
-
-function pluralRuleParser(rule, number) {
-       'use strict';
-
-       /*
-       Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
-       -----------------------------------------------------------------
-       condition     = and_condition ('or' and_condition)*
-               ('@integer' samples)?
-               ('@decimal' samples)?
-       and_condition = relation ('and' relation)*
-       relation      = is_relation | in_relation | within_relation
-       is_relation   = expr 'is' ('not')? value
-       in_relation   = expr (('not')? 'in' | '=' | '!=') range_list
-       within_relation = expr ('not')? 'within' range_list
-       expr          = operand (('mod' | '%') value)?
-       operand       = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
-       range_list    = (range | value) (',' range_list)*
-       value         = digit+
-       digit         = 0|1|2|3|4|5|6|7|8|9
-       range         = value'..'value
-       samples       = sampleRange (',' sampleRange)* (',' ('…'|'...'))?
-       sampleRange   = decimalValue '~' decimalValue
-       decimalValue  = value ('.' value)?
-       */
-
-       // We don't evaluate the samples section of the rule. Ignore it.
-       rule = rule.split('@')[0].replace(/^\s*/, '').replace(/\s*$/, '');
-
-       if (!rule.length) {
-               // Empty rule or 'other' rule.
-               return true;
-       }
-
-       // Indicates the current position in the rule as we parse through it.
-       // Shared among all parsing functions below.
-       var pos = 0,
-               operand,
-               expression,
-               relation,
-               result,
-               whitespace = makeRegexParser(/^\s+/),
-               value = makeRegexParser(/^\d+/),
-               _n_ = makeStringParser('n'),
-               _i_ = makeStringParser('i'),
-               _f_ = makeStringParser('f'),
-               _t_ = makeStringParser('t'),
-               _v_ = makeStringParser('v'),
-               _w_ = makeStringParser('w'),
-               _is_ = makeStringParser('is'),
-               _isnot_ = makeStringParser('is not'),
-               _isnot_sign_ = makeStringParser('!='),
-               _equal_ = makeStringParser('='),
-               _mod_ = makeStringParser('mod'),
-               _percent_ = makeStringParser('%'),
-               _not_ = makeStringParser('not'),
-               _in_ = makeStringParser('in'),
-               _within_ = makeStringParser('within'),
-               _range_ = makeStringParser('..'),
-               _comma_ = makeStringParser(','),
-               _or_ = makeStringParser('or'),
-               _and_ = makeStringParser('and');
-
-       function debug() {
-               // console.log.apply(console, arguments);
-       }
-
-       debug('pluralRuleParser', rule, number);
-
-       // Try parsers until one works, if none work return null
-       function choice(parserSyntax) {
-               return function() {
-                       var i, result;
-
-                       for (i = 0; i < parserSyntax.length; i++) {
-                               result = parserSyntax[i]();
-
-                               if (result !== null) {
-                                       return result;
-                               }
-                       }
-
-                       return null;
-               };
-       }
-
-       // Try several parserSyntax-es in a row.
-       // All must succeed; otherwise, return null.
-       // This is the only eager one.
-       function sequence(parserSyntax) {
-               var i, parserRes,
-                       originalPos = pos,
-                       result = [];
-
-               for (i = 0; i < parserSyntax.length; i++) {
-                       parserRes = parserSyntax[i]();
-
-                       if (parserRes === null) {
-                               pos = originalPos;
-
-                               return null;
-                       }
-
-                       result.push(parserRes);
-               }
-
-               return result;
-       }
-
-       // Run the same parser over and over until it fails.
-       // Must succeed a minimum of n times; otherwise, return null.
-       function nOrMore(n, p) {
-               return function() {
-                       var originalPos = pos,
-                               result = [],
-                               parsed = p();
-
-                       while (parsed !== null) {
-                               result.push(parsed);
-                               parsed = p();
-                       }
-
-                       if (result.length < n) {
-                               pos = originalPos;
-
-                               return null;
-                       }
-
-                       return result;
-               };
-       }
-
-       // Helpers - just make parserSyntax out of simpler JS builtin types
-       function makeStringParser(s) {
-               var len = s.length;
-
-               return function() {
-                       var result = null;
-
-                       if (rule.substr(pos, len) === s) {
-                               result = s;
-                               pos += len;
-                       }
-
-                       return result;
-               };
-       }
-
-       function makeRegexParser(regex) {
-               return function() {
-                       var matches = rule.substr(pos).match(regex);
-
-                       if (matches === null) {
-                               return null;
-                       }
-
-                       pos += matches[0].length;
-
-                       return matches[0];
-               };
-       }
-
-       /**
-        * Integer digits of n.
-        */
-       function i() {
-               var result = _i_();
-
-               if (result === null) {
-                       debug(' -- failed i', parseInt(number, 10));
-
-                       return result;
-               }
-
-               result = parseInt(number, 10);
-               debug(' -- passed i ', result);
-
-               return result;
-       }
-
-       /**
-        * Absolute value of the source number (integer and decimals).
-        */
-       function n() {
-               var result = _n_();
-
-               if (result === null) {
-                       debug(' -- failed n ', number);
-
-                       return result;
-               }
-
-               result = parseFloat(number, 10);
-               debug(' -- passed n ', result);
-
-               return result;
-       }
-
-       /**
-        * Visible fractional digits in n, with trailing zeros.
-        */
-       function f() {
-               var result = _f_();
-
-               if (result === null) {
-                       debug(' -- failed f ', number);
-
-                       return result;
-               }
-
-               result = (number + '.').split('.')[1] || 0;
-               debug(' -- passed f ', result);
-
-               return result;
-       }
-
-       /**
-        * Visible fractional digits in n, without trailing zeros.
-        */
-       function t() {
-               var result = _t_();
-
-               if (result === null) {
-                       debug(' -- failed t ', number);
-
-                       return result;
-               }
-
-               result = (number + '.').split('.')[1].replace(/0$/, '') || 0;
-               debug(' -- passed t ', result);
-
-               return result;
-       }
-
-       /**
-        * Number of visible fraction digits in n, with trailing zeros.
-        */
-       function v() {
-               var result = _v_();
-
-               if (result === null) {
-                       debug(' -- failed v ', number);
-
-                       return result;
-               }
-
-               result = (number + '.').split('.')[1].length || 0;
-               debug(' -- passed v ', result);
-
-               return result;
-       }
-
-       /**
-        * Number of visible fraction digits in n, without trailing zeros.
-        */
-       function w() {
-               var result = _w_();
-
-               if (result === null) {
-                       debug(' -- failed w ', number);
-
-                       return result;
-               }
-
-               result = (number + '.').split('.')[1].replace(/0$/, '').length || 0;
-               debug(' -- passed w ', result);
-
-               return result;
-       }
-
-       // operand       = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
-       operand = choice([n, i, f, t, v, w]);
-
-       // expr          = operand (('mod' | '%') value)?
-       expression = choice([mod, operand]);
-
-       function mod() {
-               var result = sequence(
-                       [operand, whitespace, choice([_mod_, _percent_]), whitespace, value]
-               );
-
-               if (result === null) {
-                       debug(' -- failed mod');
-
-                       return null;
-               }
-
-               debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
-
-               return parseInt(result[0], 10) % parseInt(result[4], 10);
-       }
-
-       function not() {
-               var result = sequence([whitespace, _not_]);
-
-               if (result === null) {
-                       debug(' -- failed not');
-
-                       return null;
-               }
-
-               return result[1];
-       }
-
-       // is_relation   = expr 'is' ('not')? value
-       function is() {
-               var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
-
-               if (result !== null) {
-                       debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10));
-
-                       return result[0] === parseInt(result[4], 10);
-               }
-
-               debug(' -- failed is');
-
-               return null;
-       }
-
-       // is_relation   = expr 'is' ('not')? value
-       function isnot() {
-               var result = sequence(
-                       [expression, whitespace, choice([_isnot_, _isnot_sign_]), whitespace, value]
-               );
-
-               if (result !== null) {
-                       debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
-
-                       return result[0] !== parseInt(result[4], 10);
-               }
-
-               debug(' -- failed isnot');
-
-               return null;
-       }
-
-       function not_in() {
-               var i, range_list,
-                       result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
-
-               if (result !== null) {
-                       debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]);
-                       range_list = result[4];
-
-                       for (i = 0; i < range_list.length; i++) {
-                               if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
-                                       return false;
-                               }
-                       }
-
-                       return true;
-               }
-
-               debug(' -- failed not_in');
-
-               return null;
-       }
-
-       // range_list    = (range | value) (',' range_list)*
-       function rangeList() {
-               var result = sequence([choice([range, value]), nOrMore(0, rangeTail)]),
-                       resultList = [];
-
-               if (result !== null) {
-                       resultList = resultList.concat(result[0]);
-
-                       if (result[1][0]) {
-                               resultList = resultList.concat(result[1][0]);
-                       }
-
-                       return resultList;
-               }
-
-               debug(' -- failed rangeList');
-
-               return null;
-       }
-
-       function rangeTail() {
-               // ',' range_list
-               var result = sequence([_comma_, rangeList]);
-
-               if (result !== null) {
-                       return result[1];
-               }
-
-               debug(' -- failed rangeTail');
-
-               return null;
-       }
-
-       // range         = value'..'value
-       function range() {
-               var i, array, left, right,
-                       result = sequence([value, _range_, value]);
-
-               if (result !== null) {
-                       debug(' -- passed range');
-
-                       array = [];
-                       left = parseInt(result[0], 10);
-                       right = parseInt(result[2], 10);
-
-                       for (i = left; i <= right; i++) {
-                               array.push(i);
-                       }
-
-                       return array;
-               }
-
-               debug(' -- failed range');
-
-               return null;
-       }
-
-       function _in() {
-               var result, range_list, i;
-
-               // in_relation   = expr ('not')? 'in' range_list
-               result = sequence(
-                       [expression, nOrMore(0, not), whitespace, choice([_in_, _equal_]), whitespace, rangeList]
-               );
-
-               if (result !== null) {
-                       debug(' -- passed _in:' + result);
-
-                       range_list = result[5];
-
-                       for (i = 0; i < range_list.length; i++) {
-                               if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
-                                       return (result[1][0] !== 'not');
-                               }
-                       }
-
-                       return (result[1][0] === 'not');
-               }
-
-               debug(' -- failed _in ');
-
-               return null;
-       }
-
-       /**
-        * The difference between "in" and "within" is that
-        * "in" only includes integers in the specified range,
-        * while "within" includes all values.
-        */
-       function within() {
-               var range_list, result;
-
-               // within_relation = expr ('not')? 'within' range_list
-               result = sequence(
-                       [expression, nOrMore(0, not), whitespace, _within_, whitespace, rangeList]
-               );
-
-               if (result !== null) {
-                       debug(' -- passed within');
-
-                       range_list = result[5];
-
-                       if ((result[0] >= parseInt(range_list[0], 10)) &&
-                               (result[0] < parseInt(range_list[range_list.length - 1], 10))) {
-
-                               return (result[1][0] !== 'not');
-                       }
-
-                       return (result[1][0] === 'not');
-               }
-
-               debug(' -- failed within ');
-
-               return null;
-       }
-
-       // relation      = is_relation | in_relation | within_relation
-       relation = choice([is, not_in, isnot, _in, within]);
-
-       // and_condition = relation ('and' relation)*
-       function and() {
-               var i,
-                       result = sequence([relation, nOrMore(0, andTail)]);
-
-               if (result) {
-                       if (!result[0]) {
-                               return false;
-                       }
-
-                       for (i = 0; i < result[1].length; i++) {
-                               if (!result[1][i]) {
-                                       return false;
-                               }
-                       }
-
-                       return true;
-               }
-
-               debug(' -- failed and');
-
-               return null;
-       }
-
-       // ('and' relation)*
-       function andTail() {
-               var result = sequence([whitespace, _and_, whitespace, relation]);
-
-               if (result !== null) {
-                       debug(' -- passed andTail' + result);
-
-                       return result[3];
-               }
-
-               debug(' -- failed andTail');
-
-               return null;
-
-       }
-       //  ('or' and_condition)*
-       function orTail() {
-               var result = sequence([whitespace, _or_, whitespace, and]);
-
-               if (result !== null) {
-                       debug(' -- passed orTail: ' + result[3]);
-
-                       return result[3];
-               }
-
-               debug(' -- failed orTail');
-
-               return null;
-       }
-
-       // condition     = and_condition ('or' and_condition)*
-       function condition() {
-               var i,
-                       result = sequence([and, nOrMore(0, orTail)]);
-
-               if (result) {
-                       for (i = 0; i < result[1].length; i++) {
-                               if (result[1][i]) {
-                                       return true;
-                               }
-                       }
-
-                       return result[0];
-               }
-
-               return false;
-       }
-
-       result = condition();
-
-       /**
-        * For success, the pos must have gotten to the end of the rule
-        * and returned a non-null.
-        * n.b. This is part of language infrastructure,
-        * so we do not throw an internationalizable message.
-        */
-       if (result === null) {
-               throw new Error('Parse error at position ' + pos.toString() + ' for rule: ' + rule);
-       }
-
-       if (pos !== rule.length) {
-               debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
-       }
-
-       return result;
-}
-
-/* pluralRuleParser ends here */
-mw.libs.pluralRuleParser = pluralRuleParser;
-module.exports = pluralRuleParser;
-
-} )( mediaWiki );
diff --git a/resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js b/resources/src/mediawiki.libs/mediawiki.libs.jpegmeta.js
deleted file mode 100644 (file)
index d837420..0000000
+++ /dev/null
@@ -1,742 +0,0 @@
-/**
- * This is JsJpegMeta v1.0
- * From: https://code.google.com/p/jsjpegmeta/downloads/list
- * From: https://github.com/bennoleslie/jsjpegmeta/blob/v1.0.0/jpegmeta.js
- *
- * Ported to MediaWiki ResourceLoader by Bryan Tong Minh
- * Changes:
- * - Add closure.
- * - Add this.JpegMeta assignment to expose it as global.
- * - Add export as module.
- * - Add mw.libs.jpegmeta wrapper.
- */
-
-( function ( mw ) {
-       /*
-       Copyright (c) 2009 Ben Leslie
-       
-       Permission is hereby granted, free of charge, to any person obtaining a copy
-       of this software and associated documentation files (the "Software"), to deal
-       in the Software without restriction, including without limitation the rights
-       to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-       copies of the Software, and to permit persons to whom the Software is
-       furnished to do so, subject to the following conditions:
-       
-       The above copyright notice and this permission notice shall be included in
-       all copies or substantial portions of the Software.
-       
-       THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-       IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-       FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-       AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-       LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-       OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-       THE SOFTWARE.
-       */
-       
-       /*
-        This JavaScript library is used to parse meta-data from files 
-        with mime-type image/jpeg.
-       
-        Include it with something like:
-       
-          <script type="text/javascript" src="jpegmeta.js"></script>
-       
-        This adds a single 'module' object called 'JpegMeta' to the global
-        namespace.
-       
-        Public Functions
-        ----------------
-        JpegMeta.parseNum - parse unsigned integers from binary data
-        JpegMeta.parseSnum - parse signed integers from binary data
-       
-        Public Classes
-        --------------
-        JpegMeta.Rational - A rational number class
-        JpegMeta.JfifSegment
-        JpegMeta.ExifSegment
-        JpegMeta.JpegFile - Primary class for Javascript parsing
-       */
-
-       var JpegMeta = {};
-       // MediaWiki: Expose as global
-       this.JpegMeta = JpegMeta;
-       
-       /* 
-          parse an unsigned number of size bytes at offset in some binary string data.
-          If endian
-          is "<" parse the data as little endian, if endian
-          is ">" parse as big-endian.
-       */
-       JpegMeta.parseNum = function parseNum(endian, data, offset, size) {
-           var i;
-           var ret;
-           var big_endian = (endian === ">");
-           if (offset === undefined) offset = 0;
-           if (size === undefined) size = data.length - offset;
-           for (big_endian ? i = offset : i = offset + size - 1; 
-                big_endian ? i < offset + size : i >= offset; 
-                big_endian ? i++ : i--) {
-               ret <<= 8;
-               ret += data.charCodeAt(i);
-           }
-           return ret;
-       };
-       
-       /* 
-          parse an signed number of size bytes at offset in some binary string data.
-          If endian
-          is "<" parse the data as little endian, if endian
-          is ">" parse as big-endian.
-       */
-       JpegMeta.parseSnum = function parseSnum(endian, data, offset, size) {
-           var i;
-           var ret;
-           var neg;
-           var big_endian = (endian === ">");
-           if (offset === undefined) offset = 0;
-           if (size === undefined) size = data.length - offset;
-           for (big_endian ? i = offset : i = offset + size - 1; 
-                big_endian ? i < offset + size : i >= offset; 
-                big_endian ? i++ : i--) {
-               if (neg === undefined) {
-                   /* Negative if top bit is set */
-                   neg = (data.charCodeAt(i) & 0x80) === 0x80;
-               }
-               ret <<= 8;
-               /* If it is negative we invert the bits */
-               ret += neg ? ~data.charCodeAt(i) & 0xff: data.charCodeAt(i);
-           }
-           if (neg) {
-               /* If it is negative we do two's complement */
-               ret += 1;
-               ret *= -1;
-           }
-           return ret;
-       };
-       
-       /* Rational number class */
-       JpegMeta.Rational = function Rational(num, den)
-       {
-           this.num = num;
-           this.den = den || 1;
-           return this;
-       };
-       
-       /* Rational number methods */
-       JpegMeta.Rational.prototype.toString = function toString() {
-           if (this.num === 0) {
-               return "" + this.num;
-           }
-           if (this.den === 1) {
-               return "" + this.num;
-           }
-           if (this.num === 1) {
-               return this.num + " / " + this.den;
-           }
-           return this.num / this.den; // + "/" + this.den;
-       };
-       
-       JpegMeta.Rational.prototype.asFloat = function asFloat() {
-           return this.num / this.den;
-       };
-       
-       /* MetaGroup class */
-       JpegMeta.MetaGroup = function MetaGroup(fieldName, description) {
-           this.fieldName = fieldName;
-           this.description = description;
-           this.metaProps = {};
-           return this;
-       };
-       
-       JpegMeta.MetaGroup.prototype._addProperty = function _addProperty(fieldName, description, value) {
-           var property = new JpegMeta.MetaProp(fieldName, description, value);
-           this[property.fieldName] = property;
-           this.metaProps[property.fieldName] = property;
-       };
-       
-       JpegMeta.MetaGroup.prototype.toString = function toString() {
-           return "[MetaGroup " + this.description + "]";
-       };
-
-       /* MetaProp class */
-       JpegMeta.MetaProp = function MetaProp(fieldName, description, value) {
-           this.fieldName = fieldName;
-           this.description = description;
-           this.value = value;
-           return this;
-       };
-       
-       JpegMeta.MetaProp.prototype.toString = function toString() {
-           return "" + this.value;
-       };
-
-       /* JpegFile class */
-       JpegMeta.JpegFile = function JpegFile(binary_data, filename) {
-           /* Change this to EOI if we want to parse. */
-           var break_segment = this._SOS;
-           
-           this.metaGroups = {};
-           this._binary_data = binary_data;
-           this.filename = filename;
-           
-           /* Go through and parse. */
-           var pos = 0;
-           var pos_start_of_segment = 0;
-           var delim;
-           var mark;
-           var _mark;
-           var segsize;
-           var headersize;
-           var mark_code;
-           var mark_fn;
-       
-           /* Check to see if this looks like a JPEG file */
-           if (this._binary_data.slice(0, 2) !== this._SOI_MARKER) {
-               throw new Error("Doesn't look like a JPEG file. First two bytes are " + 
-                               this._binary_data.charCodeAt(0) + "," + 
-                               this._binary_data.charCodeAt(1) + ".");
-           }
-           
-           pos += 2;
-           
-           while (pos < this._binary_data.length) {
-               delim = this._binary_data.charCodeAt(pos++);
-               mark = this._binary_data.charCodeAt(pos++);
-               
-               pos_start_of_segment = pos;
-               
-               if (delim != this._DELIM) {
-                   break;
-               }
-               
-               if (mark === break_segment) {
-                   break;
-               }
-               
-               headersize = JpegMeta.parseNum(">", this._binary_data, pos, 2);
-               
-               /* Find the end */
-               pos += headersize;
-               while (pos < this._binary_data.length) {
-                   delim = this._binary_data.charCodeAt(pos++);
-                   if (delim == this._DELIM) {
-                       _mark = this._binary_data.charCodeAt(pos++);
-                       if (_mark != 0x0) {
-                           pos -= 2;
-                           break;
-                       }
-                   }
-               }
-               
-               segsize = pos - pos_start_of_segment;
-               
-               if (this._markers[mark]) {
-                   mark_code = this._markers[mark][0];
-                   mark_fn = this._markers[mark][1];
-               } else {
-                   mark_code = "UNKN";
-                   mark_fn = undefined;
-               }
-               
-               if (mark_fn) {
-                   this[mark_fn](mark, pos_start_of_segment + 2);
-               }
-               
-           }
-           
-           if (this.general === undefined) {
-               throw Error("Invalid JPEG file.");
-           }
-           
-           return this;
-       };
-       
-       this.JpegMeta.JpegFile.prototype.toString = function () {
-           return "[JpegFile " + this.filename + " " + 
-               this.general.type + " " + 
-               this.general.pixelWidth + "x" + 
-               this.general.pixelHeight +
-               " Depth: " + this.general.depth + "]";
-       };
-       
-       /* Some useful constants */
-       this.JpegMeta.JpegFile.prototype._SOI_MARKER = '\xff\xd8';
-       this.JpegMeta.JpegFile.prototype._DELIM = 0xff;
-       this.JpegMeta.JpegFile.prototype._EOI = 0xd9;
-       this.JpegMeta.JpegFile.prototype._SOS = 0xda;
-       
-       this.JpegMeta.JpegFile.prototype._sofHandler = function _sofHandler (mark, pos) {
-           if (this.general !== undefined) {
-               throw Error("Unexpected multiple-frame image");
-           }
-       
-           this._addMetaGroup("general", "General");
-           this.general._addProperty("depth", "Depth", JpegMeta.parseNum(">", this._binary_data, pos, 1));
-           this.general._addProperty("pixelHeight", "Pixel Height", JpegMeta.parseNum(">", this._binary_data, pos + 1, 2));
-           this.general._addProperty("pixelWidth", "Pixel Width",JpegMeta.parseNum(">", this._binary_data, pos + 3, 2));
-           this.general._addProperty("type", "Type", this._markers[mark][2]);
-       };
-       
-       /* JFIF idents */
-       this.JpegMeta.JpegFile.prototype._JFIF_IDENT = "JFIF\x00";
-       this.JpegMeta.JpegFile.prototype._JFXX_IDENT = "JFXX\x00";
-       
-       /* Exif idents */
-       this.JpegMeta.JpegFile.prototype._EXIF_IDENT = "Exif\x00";
-       
-       /* TIFF types */
-       this.JpegMeta.JpegFile.prototype._types = {
-           /* The format is identifier : ["type name", type_size_in_bytes ] */
-           1 : ["BYTE", 1],
-           2 : ["ASCII", 1],
-           3 : ["SHORT", 2],
-           4 : ["LONG", 4],
-           5 : ["RATIONAL", 8],
-           6 : ["SBYTE", 1],
-           7 : ["UNDEFINED", 1],
-           8 : ["SSHORT", 2],
-           9 : ["SLONG", 4],
-           10 : ["SRATIONAL", 8],
-           11 : ["FLOAT", 4],
-           12 : ["DOUBLE", 8]
-       };
-       
-       this.JpegMeta.JpegFile.prototype._tifftags = {
-           /* A. Tags relating to image data structure */
-           256 : ["Image width", "ImageWidth"],
-           257 : ["Image height", "ImageLength"],
-           258 : ["Number of bits per component", "BitsPerSample"],
-           259 : ["Compression scheme", "Compression", 
-                  {1 : "uncompressed", 6 : "JPEG compression" }],
-           262 : ["Pixel composition", "PhotmetricInerpretation",
-                  {2 : "RGB", 6 : "YCbCr"}],
-           274 : ["Orientation of image", "Orientation",
-                  /* FIXME: Check the mirror-image / reverse encoding and rotation */
-                  {1 : "Normal", 2 : "Reverse?", 
-                   3 : "Upside-down", 4 : "Upside-down Reverse",
-                   5 : "90 degree CW", 6 : "90 degree CW reverse",
-                   7 : "90 degree CCW", 8 : "90 degree CCW reverse"}],
-           277 : ["Number of components", "SamplesPerPixel"],
-           284 : ["Image data arrangement", "PlanarConfiguration",
-                  {1 : "chunky format", 2 : "planar format"}],
-           530 : ["Subsampling ratio of Y to C", "YCbCrSubSampling"],
-           531 : ["Y and C positioning", "YCbCrPositioning",
-                  {1 : "centered", 2 : "co-sited"}],
-           282 : ["X Resolution", "XResolution"],
-           283 : ["Y Resolution", "YResolution"],
-           296 : ["Resolution Unit", "ResolutionUnit",
-                  {2 : "inches", 3 : "centimeters"}],
-           /* B. Tags realting to recording offset */
-           273 : ["Image data location", "StripOffsets"],
-           278 : ["Number of rows per strip", "RowsPerStrip"],
-           279 : ["Bytes per compressed strip", "StripByteCounts"],
-           513 : ["Offset to JPEG SOI", "JPEGInterchangeFormat"],
-           514 : ["Bytes of JPEG Data", "JPEGInterchangeFormatLength"],
-           /* C. Tags relating to image data characteristics */
-           301 : ["Transfer function", "TransferFunction"],
-           318 : ["White point chromaticity", "WhitePoint"],
-           319 : ["Chromaticities of primaries", "PrimaryChromaticities"],
-           529 : ["Color space transformation matrix coefficients", "YCbCrCoefficients"],
-           532 : ["Pair of black and white reference values", "ReferenceBlackWhite"],
-           /* D. Other tags */
-           306 : ["Date and time", "DateTime"],
-           270 : ["Image title", "ImageDescription"],
-           271 : ["Make", "Make"],
-           272 : ["Model", "Model"],
-           305 : ["Software", "Software"],
-           315 : ["Person who created the image", "Artist"],
-           316 : ["Host Computer", "HostComputer"],
-           33432 : ["Copyright holder", "Copyright"],
-           
-           34665 : ["Exif tag", "ExifIfdPointer"],
-           34853 : ["GPS tag", "GPSInfoIfdPointer"]
-       };
-       
-       this.JpegMeta.JpegFile.prototype._exiftags = {
-           /* Tag Support Levels (2) - 0th IFX Exif Private Tags */
-           /* A. Tags Relating to Version */
-           36864 : ["Exif Version", "ExifVersion"],
-           40960 : ["FlashPix Version", "FlashpixVersion"],
-           
-           /* B. Tag Relating to Image Data Characteristics */
-           40961 : ["Color Space", "ColorSpace"],
-           
-           /* C. Tags Relating to Image Configuration */
-           37121 : ["Meaning of each component", "ComponentsConfiguration"],
-           37122 : ["Compressed Bits Per Pixel", "CompressedBitsPerPixel"],
-           40962 : ["Pixel X Dimension", "PixelXDimension"],
-           40963 : ["Pixel Y Dimension", "PixelYDimension"],
-           
-           /* D. Tags Relating to User Information */
-           37500 : ["Manufacturer notes", "MakerNote"],
-           37510 : ["User comments", "UserComment"],
-           
-           /* E. Tag Relating to Related File Information */
-           40964 : ["Related audio file", "RelatedSoundFile"],
-           
-           /* F. Tags Relating to Date and Time */
-           36867 : ["Date Time Original", "DateTimeOriginal"],
-           36868 : ["Date Time Digitized", "DateTimeDigitized"],
-           37520 : ["DateTime subseconds", "SubSecTime"],
-           37521 : ["DateTimeOriginal subseconds", "SubSecTimeOriginal"],
-           37522 : ["DateTimeDigitized subseconds", "SubSecTimeDigitized"],
-           
-           /* G. Tags Relating to Picture-Taking Conditions */
-           33434 : ["Exposure time", "ExposureTime"],
-           33437 : ["FNumber", "FNumber"],
-           34850 : ["Exposure program", "ExposureProgram"],
-           34852 : ["Spectral sensitivity", "SpectralSensitivity"],
-           34855 : ["ISO Speed Ratings", "ISOSpeedRatings"],
-           34856 : ["Optoelectric coefficient", "OECF"],
-           37377 : ["Shutter Speed",  "ShutterSpeedValue"],
-           37378 : ["Aperture Value", "ApertureValue"],
-           37379 : ["Brightness", "BrightnessValue"],
-           37380 : ["Exposure Bias Value", "ExposureBiasValue"],
-           37381 : ["Max Aperture Value", "MaxApertureValue"],
-           37382 : ["Subject Distance", "SubjectDistance"],
-           37383 : ["Metering Mode", "MeteringMode"],
-           37384 : ["Light Source", "LightSource"],
-           37385 : ["Flash", "Flash"],
-           37386 : ["Focal Length", "FocalLength"],
-           37396 : ["Subject Area", "SubjectArea"],
-           41483 : ["Flash Energy", "FlashEnergy"],
-           41484 : ["Spatial Frequency Response", "SpatialFrequencyResponse"],
-           41486 : ["Focal Plane X Resolution", "FocalPlaneXResolution"],
-           41487 : ["Focal Plane Y Resolution", "FocalPlaneYResolution"],
-           41488 : ["Focal Plane Resolution Unit", "FocalPlaneResolutionUnit"],
-           41492 : ["Subject Location", "SubjectLocation"],
-           41493 : ["Exposure Index", "ExposureIndex"],
-           41495 : ["Sensing Method", "SensingMethod"],
-           41728 : ["File Source", "FileSource"],
-           41729 : ["Scene Type", "SceneType"],
-           41730 : ["CFA Pattern", "CFAPattern"],
-           41985 : ["Custom Rendered", "CustomRendered"],
-           41986 : ["Exposure Mode", "Exposure Mode"],
-           41987 : ["White Balance", "WhiteBalance"],
-           41988 : ["Digital Zoom Ratio", "DigitalZoomRatio"],
-           41990 : ["Scene Capture Type", "SceneCaptureType"],
-           41991 : ["Gain Control", "GainControl"],
-           41992 : ["Contrast", "Contrast"],
-           41993 : ["Saturation", "Saturation"],
-           41994 : ["Sharpness", "Sharpness"],
-           41995 : ["Device settings description", "DeviceSettingDescription"],
-           41996 : ["Subject distance range", "SubjectDistanceRange"],
-           
-           /* H. Other Tags */
-           42016 : ["Unique image ID", "ImageUniqueID"],
-           
-           40965 : ["Interoperability tag", "InteroperabilityIFDPointer"]
-       };
-       
-       this.JpegMeta.JpegFile.prototype._gpstags = {
-           /* A. Tags Relating to GPS */
-           0 : ["GPS tag version", "GPSVersionID"],
-           1 : ["North or South Latitude", "GPSLatitudeRef"],
-           2 : ["Latitude", "GPSLatitude"],
-           3 : ["East or West Longitude", "GPSLongitudeRef"],
-           4 : ["Longitude", "GPSLongitude"],
-           5 : ["Altitude reference", "GPSAltitudeRef"],
-           6 : ["Altitude", "GPSAltitude"],
-           7 : ["GPS time (atomic clock)", "GPSTimeStamp"],
-           8 : ["GPS satellites usedd for measurement", "GPSSatellites"],
-           9 : ["GPS receiver status", "GPSStatus"],
-           10 : ["GPS mesaurement mode", "GPSMeasureMode"],
-           11 : ["Measurement precision", "GPSDOP"],
-           12 : ["Speed unit", "GPSSpeedRef"],
-           13 : ["Speed of GPS receiver", "GPSSpeed"],
-           14 : ["Reference for direction of movement", "GPSTrackRef"],
-           15 : ["Direction of movement", "GPSTrack"],
-           16 : ["Reference for direction of image", "GPSImgDirectionRef"],
-           17 : ["Direction of image", "GPSImgDirection"],
-           18 : ["Geodetic survey data used", "GPSMapDatum"],
-           19 : ["Reference for latitude of destination", "GPSDestLatitudeRef"],
-           20 : ["Latitude of destination", "GPSDestLatitude"],
-           21 : ["Reference for longitude of destination", "GPSDestLongitudeRef"],
-           22 : ["Longitude of destination", "GPSDestLongitude"],
-           23 : ["Reference for bearing of destination", "GPSDestBearingRef"],
-           24 : ["Bearing of destination", "GPSDestBearing"],
-           25 : ["Reference for distance to destination", "GPSDestDistanceRef"],
-           26 : ["Distance to destination", "GPSDestDistance"],
-           27 : ["Name of GPS processing method", "GPSProcessingMethod"],
-           28 : ["Name of GPS area", "GPSAreaInformation"],
-           29 : ["GPS Date", "GPSDateStamp"],
-           30 : ["GPS differential correction", "GPSDifferential"]
-       };
-
-       this.JpegMeta.JpegFile.prototype._markers = {
-           /* Start Of Frame markers, non-differential, Huffman coding */
-           0xc0: ["SOF0", "_sofHandler", "Baseline DCT"],
-           0xc1: ["SOF1", "_sofHandler", "Extended sequential DCT"],
-           0xc2: ["SOF2", "_sofHandler", "Progressive DCT"],
-           0xc3: ["SOF3", "_sofHandler", "Lossless (sequential)"],
-           
-           /* Start Of Frame markers, differential, Huffman coding */
-           0xc5: ["SOF5", "_sofHandler", "Differential sequential DCT"],
-           0xc6: ["SOF6", "_sofHandler", "Differential progressive DCT"],
-           0xc7: ["SOF7", "_sofHandler", "Differential lossless (sequential)"],
-           
-           /* Start Of Frame markers, non-differential, arithmetic coding */
-           0xc8: ["JPG", null, "Reserved for JPEG extensions"],
-           0xc9: ["SOF9", "_sofHandler", "Extended sequential DCT"],
-           0xca: ["SOF10", "_sofHandler", "Progressive DCT"],
-           0xcb: ["SOF11", "_sofHandler", "Lossless (sequential)"],
-           
-           /* Start Of Frame markers, differential, arithmetic coding */
-           0xcd: ["SOF13", "_sofHandler", "Differential sequential DCT"],
-           0xce: ["SOF14", "_sofHandler", "Differential progressive DCT"],
-           0xcf: ["SOF15", "_sofHandler", "Differential lossless (sequential)"],
-           
-           /* Huffman table specification */
-           0xc4: ["DHT", null, "Define Huffman table(s)"],
-           0xcc: ["DAC", null, "Define arithmetic coding conditioning(s)"],
-           
-           /* Restart interval termination" */
-           0xd0: ["RST0", null, "Restart with modulo 8 count “0”"],
-           0xd1: ["RST1", null, "Restart with modulo 8 count “1”"],
-           0xd2: ["RST2", null, "Restart with modulo 8 count “2”"],
-           0xd3: ["RST3", null, "Restart with modulo 8 count “3”"],
-           0xd4: ["RST4", null, "Restart with modulo 8 count “4”"],
-           0xd5: ["RST5", null, "Restart with modulo 8 count “5”"],
-           0xd6: ["RST6", null, "Restart with modulo 8 count “6”"],
-           0xd7: ["RST7", null, "Restart with modulo 8 count “7”"],
-           
-           /* Other markers */
-           0xd8: ["SOI", null, "Start of image"],
-           0xd9: ["EOI", null, "End of image"],
-           0xda: ["SOS", null, "Start of scan"],
-           0xdb: ["DQT", null, "Define quantization table(s)"],
-           0xdc: ["DNL", null, "Define number of lines"],
-           0xdd: ["DRI", null, "Define restart interval"],
-           0xde: ["DHP", null, "Define hierarchical progression"],
-           0xdf: ["EXP", null, "Expand reference component(s)"],
-           0xe0: ["APP0", "_app0Handler", "Reserved for application segments"],
-           0xe1: ["APP1", "_app1Handler"],
-           0xe2: ["APP2", null],
-           0xe3: ["APP3", null],
-           0xe4: ["APP4", null],
-           0xe5: ["APP5", null],
-           0xe6: ["APP6", null],
-           0xe7: ["APP7", null],
-           0xe8: ["APP8", null],
-           0xe9: ["APP9", null],
-           0xea: ["APP10", null],
-           0xeb: ["APP11", null],
-           0xec: ["APP12", null],
-           0xed: ["APP13", null],
-           0xee: ["APP14", null],
-           0xef: ["APP15", null],
-           0xf0: ["JPG0", null], /* Reserved for JPEG extensions */
-           0xf1: ["JPG1", null],
-           0xf2: ["JPG2", null],
-           0xf3: ["JPG3", null],
-           0xf4: ["JPG4", null],
-           0xf5: ["JPG5", null],
-           0xf6: ["JPG6", null],
-           0xf7: ["JPG7", null],
-           0xf8: ["JPG8", null],
-           0xf9: ["JPG9", null],
-           0xfa: ["JPG10", null],
-           0xfb: ["JPG11", null],
-           0xfc: ["JPG12", null],
-           0xfd: ["JPG13", null],
-           0xfe: ["COM", null], /* Comment */
-           
-           /* Reserved markers */
-           0x01: ["JPG13", null] /* For temporary private use in arithmetic coding */
-           /* 02 -> bf are reserverd */
-       };
-
-       /* Private methods */
-       this.JpegMeta.JpegFile.prototype._addMetaGroup = function _addMetaGroup(name, description) {
-           var group = new JpegMeta.MetaGroup(name, description);
-           this[group.fieldName] = group;
-           this.metaGroups[group.fieldName] = group;
-           return group;
-       };
-
-       this.JpegMeta.JpegFile.prototype._parseIfd = function _parseIfd(endian, _binary_data, base, ifd_offset, tags, name, description) {
-           var num_fields = JpegMeta.parseNum(endian, _binary_data, base + ifd_offset, 2);
-           /* Per tag variables */
-           var i, j;
-           var tag_base;
-           var tag_field;
-           var type, type_field, type_size;
-           var num_values;
-           var value_offset;
-           var value;
-           var _val;
-           var num;
-           var den;
-           
-           var group;
-           
-           group = this._addMetaGroup(name, description);
-       
-           for (var i = 0; i < num_fields; i++) {
-               /* parse the field */
-               tag_base = base + ifd_offset + 2 + (i * 12);
-               tag_field = JpegMeta.parseNum(endian, _binary_data, tag_base, 2);
-               type_field = JpegMeta.parseNum(endian, _binary_data, tag_base + 2, 2);
-               num_values = JpegMeta.parseNum(endian, _binary_data, tag_base + 4, 4);
-               value_offset = JpegMeta.parseNum(endian, _binary_data, tag_base + 8, 4);
-               if (this._types[type_field] === undefined) {
-                   continue;
-               }
-               type = this._types[type_field][0];
-               type_size = this._types[type_field][1];
-               
-               if (type_size * num_values <= 4) {
-                   /* Data is in-line */
-                   value_offset = tag_base + 8;
-               } else {
-                   value_offset = base + value_offset;
-               }
-               
-               /* Read the value */
-               if (type == "UNDEFINED") {
-                   value = _binary_data.slice(value_offset, value_offset + num_values);
-               } else if (type == "ASCII") {
-                   value = _binary_data.slice(value_offset, value_offset + num_values);
-                   value = value.split('\x00')[0];
-                   /* strip trail nul */
-               } else {
-                   value = new Array();
-                   for (j = 0; j < num_values; j++, value_offset += type_size) {
-                       if (type == "BYTE" || type == "SHORT" || type == "LONG") {
-                           value.push(JpegMeta.parseNum(endian, _binary_data, value_offset, type_size));
-                       }
-                       if (type == "SBYTE" || type == "SSHORT" || type == "SLONG") {
-                           value.push(JpegMeta.parseSnum(endian, _binary_data, value_offset, type_size));
-                       }
-                       if (type == "RATIONAL") {
-                           num = JpegMeta.parseNum(endian, _binary_data, value_offset, 4);
-                           den = JpegMeta.parseNum(endian, _binary_data, value_offset + 4, 4);
-                           value.push(new JpegMeta.Rational(num, den));
-                       }
-                       if (type == "SRATIONAL") {
-                           num = JpegMeta.parseSnum(endian, _binary_data, value_offset, 4);
-                           den = JpegMeta.parseSnum(endian, _binary_data, value_offset + 4, 4);
-                           value.push(new JpegMeta.Rational(num, den));
-                       }
-                       value.push();
-                   }
-                   if (num_values === 1) {
-                       value = value[0];
-                   }
-               }
-               if (tags[tag_field] !== undefined) {
-                       group._addProperty(tags[tag_field][1], tags[tag_field][0], value);
-               }
-           }
-       };
-
-       this.JpegMeta.JpegFile.prototype._jfifHandler = function _jfifHandler(mark, pos) {
-           if (this.jfif !== undefined) {
-               throw Error("Multiple JFIF segments found");
-           }
-           this._addMetaGroup("jfif", "JFIF");
-           this.jfif._addProperty("version_major", "Version Major", this._binary_data.charCodeAt(pos + 5));
-           this.jfif._addProperty("version_minor", "Version Minor", this._binary_data.charCodeAt(pos + 6));
-           this.jfif._addProperty("version", "JFIF Version", this.jfif.version_major.value + "." + this.jfif.version_minor.value);
-           this.jfif._addProperty("units", "Density Unit", this._binary_data.charCodeAt(pos + 7));
-           this.jfif._addProperty("Xdensity", "X density", JpegMeta.parseNum(">", this._binary_data, pos + 8, 2));
-           this.jfif._addProperty("Ydensity", "Y Density", JpegMeta.parseNum(">", this._binary_data, pos + 10, 2));
-           this.jfif._addProperty("Xthumbnail", "X Thumbnail", JpegMeta.parseNum(">", this._binary_data, pos + 12, 1));
-           this.jfif._addProperty("Ythumbnail", "Y Thumbnail", JpegMeta.parseNum(">", this._binary_data, pos + 13, 1));
-       };
-
-       /* Handle app0 segments */
-       this.JpegMeta.JpegFile.prototype._app0Handler = function app0Handler(mark, pos) {
-           var ident = this._binary_data.slice(pos, pos + 5);
-           if (ident == this._JFIF_IDENT) {
-               this._jfifHandler(mark, pos);
-           } else if (ident == this._JFXX_IDENT) {
-               /* Don't handle JFXX Ident yet */
-           } else {
-               /* Don't know about other idents */
-           }
-       };
-
-       /* Handle app1 segments */
-       this.JpegMeta.JpegFile.prototype._app1Handler = function _app1Handler(mark, pos) {
-           var ident = this._binary_data.slice(pos, pos + 5);
-           if (ident == this._EXIF_IDENT) {
-               this._exifHandler(mark, pos + 6);
-           } else {
-               /* Don't know about other idents */
-           }
-       };
-
-       /* Handle exif segments */
-       JpegMeta.JpegFile.prototype._exifHandler = function _exifHandler(mark, pos) {
-           if (this.exif !== undefined) {
-               throw new Error("Multiple JFIF segments found");
-           }
-           
-           /* Parse this TIFF header */
-           var endian;
-           var magic_field;
-           var ifd_offset;
-           var primary_ifd, exif_ifd, gps_ifd;
-           var endian_field = this._binary_data.slice(pos, pos + 2);
-           
-           /* Trivia: This 'I' is for Intel, the 'M' is for Motorola */
-           if (endian_field === "II") {
-               endian = "<";
-           } else if (endian_field === "MM") {
-               endian = ">";
-           } else {
-               throw new Error("Malformed TIFF meta-data. Unknown endianess: " + endian_field);
-           }
-           
-           magic_field = JpegMeta.parseNum(endian, this._binary_data, pos + 2, 2);
-           
-           if (magic_field !== 42) {
-               throw new Error("Malformed TIFF meta-data. Bad magic: " + magic_field);
-           }
-           
-           ifd_offset = JpegMeta.parseNum(endian, this._binary_data, pos + 4, 4);
-           
-           /* Parse 0th IFD */
-           this._parseIfd(endian, this._binary_data, pos, ifd_offset, this._tifftags, "tiff", "TIFF");
-           
-           if (this.tiff.ExifIfdPointer) {
-               this._parseIfd(endian, this._binary_data, pos, this.tiff.ExifIfdPointer.value, this._exiftags, "exif", "Exif");
-           }
-           
-           if (this.tiff.GPSInfoIfdPointer) {
-               this._parseIfd(endian, this._binary_data, pos, this.tiff.GPSInfoIfdPointer.value, this._gpstags, "gps", "GPS");
-               if (this.gps.GPSLatitude) {
-                   var latitude;
-                   latitude = this.gps.GPSLatitude.value[0].asFloat() + 
-                       (1 / 60) * this.gps.GPSLatitude.value[1].asFloat() + 
-                       (1 / 3600) * this.gps.GPSLatitude.value[2].asFloat();
-                   if (this.gps.GPSLatitudeRef.value === "S") {
-                       latitude = -latitude;
-                   }
-                   this.gps._addProperty("latitude", "Dec. Latitude", latitude);
-               }
-               if (this.gps.GPSLongitude) {
-                   var longitude;
-                   longitude = this.gps.GPSLongitude.value[0].asFloat() + 
-                       (1 / 60) * this.gps.GPSLongitude.value[1].asFloat() + 
-                       (1 / 3600) * this.gps.GPSLongitude.value[2].asFloat();
-                   if (this.gps.GPSLongitudeRef.value === "W") {
-                       longitude = -longitude;
-                   }
-                   this.gps._addProperty("longitude", "Dec. Longitude", longitude);
-               }
-           }
-       };
-
-       // MediaWiki: Export as module
-       module.exports = function( fileReaderResult, fileName ) {
-               return new JpegMeta.JpegFile( fileReaderResult, fileName );
-       };
-
-       // MediaWiki: Add mw.libs wrapper
-       // @deprecated since 1.31
-       mw.log.deprecate( mw.libs, 'jpegmeta', module.exports );
-
-}( mediaWiki ) );
index 67d6e2c..d81df65 100644 (file)
        };
 
        /**
-        * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements).
-        * e.g.
+        * Returns a function suitable for static use, to construct strings from a message key (and optional replacements).
+        *
+        * Example:
         *
-        *       window.gM = mediaWiki.jqueryMsg.getMessageFunction( options );
-        *       $( 'p#headline' ).html( gM( 'hello-user', username ) );
+        *       var format = mediaWiki.jqueryMsg.getMessageFunction( options );
+        *       $( '#example' ).text( format( 'hello-user', username ) );
         *
-        * Like the old gM() function this returns only strings, so it destroys any bindings. If you want to preserve bindings use the
-        * jQuery plugin version instead. This is only included for backwards compatibility with gM().
+        * Tthis returns only strings, so it destroys any bindings. If you want to preserve bindings, use the
+        * jQuery plugin version instead. This was originally created to ease migration from `window.gM()`,
+        * from a time when the parser used by `mw.message` was not extendable.
         *
         * N.B. replacements are variadic arguments or an array in second parameter. In other words:
         *    somefunction( a, b, c, d )
         *    somefunction( a, [b, c, d] )
         *
         * @param {Object} options parser options
-        * @return {Function} Function suitable for assigning to window.gM
+        * @return {Function} Function The message formatter
         * @return {string} return.key Message key.
         * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
         * @return {string} return.return Rendered HTML.
                }
        };
 
-       // Deprecated! don't rely on gM existing.
-       // The window.gM ought not to be required - or if required, not required here.
-       // But moving it to extensions breaks it (?!)
-       // Need to fix plugin so it could do attributes as well, then will be okay to remove this.
-       // @deprecated since 1.23
-       mw.log.deprecate( window, 'gM', mw.jqueryMsg.getMessageFunction(), 'Use mw.message( ... ).parse() instead.' );
-
        /**
         * @method
         * @member jQuery
index fbd4530..a8c82b1 100644 (file)
                         *         'moduleName': {
                         *             // From mw.loader.register()
                         *             'version': '########' (hash)
-                        *             'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
+                        *             'dependencies': ['required.foo', 'bar.also', ...]
                         *             'group': 'somegroup', (or) null
                         *             'source': 'local', (or) 'anotherwiki'
                         *             'skip': 'return !!window.Example', (or) null
                                 */
                                jobs = [],
 
-                               // For getMarker()
-                               marker = null,
+                               /**
+                                * For #addEmbeddedCSS() and #addLink()
+                                *
+                                * @private
+                                * @property {HTMLElement|null} marker
+                                */
+                               marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ),
 
                                // For addEmbeddedCSS()
                                cssBuffer = '',
                                cssCallbacks = [],
                                rAF = window.requestAnimationFrame || setTimeout;
 
-                       function getMarker() {
-                               if ( !marker ) {
-                                       // Cache
-                                       marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' );
-                                       if ( !marker ) {
-                                               mw.log( 'Created ResourceLoaderDynamicStyles marker dynamically' );
-                                               marker = document.createElement( 'meta' );
-                                               marker.name = 'ResourceLoaderDynamicStyles';
-                                               document.head.appendChild( marker );
-                                       }
-                               }
-                               return marker;
-                       }
-
                        /**
                         * Create a new style element and add it to the DOM.
                         *
                         * @private
                         * @param {string} text CSS text
-                        * @param {Node} [nextNode] The element where the style tag
+                        * @param {Node|null} [nextNode] The element where the style tag
                         *  should be inserted before
                         * @return {HTMLElement} Reference to the created style element
                         */
                        function newStyleTag( text, nextNode ) {
-                               var s = document.createElement( 'style' );
-
-                               s.appendChild( document.createTextNode( text ) );
+                               var el = document.createElement( 'style' );
+                               el.appendChild( document.createTextNode( text ) );
                                if ( nextNode && nextNode.parentNode ) {
-                                       nextNode.parentNode.insertBefore( s, nextNode );
+                                       nextNode.parentNode.insertBefore( el, nextNode );
                                } else {
-                                       document.head.appendChild( s );
+                                       document.head.appendChild( el );
                                }
-
-                               return s;
+                               return el;
                        }
 
                        /**
                                        cssBuffer = '';
                                }
 
-                               $( newStyleTag( cssText, getMarker() ) );
+                               newStyleTag( cssText, marker );
 
                                fireCallbacks();
                        }
                                        }
                                }
 
-                               // Resolves dynamic loader function and replaces it with its own results
-                               if ( typeof registry[ module ].dependencies === 'function' ) {
-                                       registry[ module ].dependencies = registry[ module ].dependencies();
-                                       // Ensures the module's dependencies are always in an array
-                                       if ( typeof registry[ module ].dependencies !== 'object' ) {
-                                               registry[ module ].dependencies = [ registry[ module ].dependencies ];
-                                       }
-                               }
                                if ( resolved.indexOf( module ) !== -1 ) {
                                        // Module already resolved; nothing to do
                                        return;
                                // see #addEmbeddedCSS, T33676, T43331, and T49277 for details.
                                el.href = url;
 
-                               $( getMarker() ).before( el );
+                               if ( marker && marker.parentNode ) {
+                                       marker.parentNode.insertBefore( el, marker );
+                               } else {
+                                       document.head.appendChild( el );
+                               }
                        }
 
                        /**
                                 *  a list of arguments compatible with this method
                                 * @param {string|number} version Module version hash (falls backs to empty string)
                                 *  Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
-                                * @param {string|Array|Function} dependencies One string or array of strings of module
-                                *  names on which this module depends, or a function that returns that array.
+                                * @param {string|Array} dependencies One string or array of strings of module
+                                *  names on which this module depends.
                                 * @param {string} [group=null] Group which the module is in
                                 * @param {string} [source='local'] Name of the source
                                 * @param {string} [skip=null] Script body of the skip function
                                        if ( typeof dependencies === 'string' ) {
                                                // A single module name
                                                deps = [ dependencies ];
-                                       } else if ( typeof dependencies === 'object' || typeof dependencies === 'function' ) {
-                                               // Array of module names or a function that returns an array
+                                       } else if ( typeof dependencies === 'object' ) {
+                                               // Array of module names
                                                deps = dependencies;
                                        }
                                        // List the module as registered
diff --git a/resources/src/mediawiki/mediawiki.sectionAnchor.css b/resources/src/mediawiki/mediawiki.sectionAnchor.css
deleted file mode 100644 (file)
index f8f0022..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-.mw-headline-anchor {
-       display: none;
-}
index 9f6167a..f0c74ce 100644 (file)
                switch ( mode ) {
                        case 'html5':
                                return str.replace( / /g, '_' );
-                       case 'html5-legacy':
-                               str = str.replace( /[ \t\n\r\f_'"&#%]+/g, '_' )
-                                       .replace( /^_+|_+$/, '' );
-                               if ( str === '' ) {
-                                       str = '_';
-                               }
-                               return str;
                        case 'legacy':
                                return rawurlencode( str.replace( / /g, '_' ) )
                                        .replace( /%3A/g, ':' )
diff --git a/resources/src/moment-dmy.js b/resources/src/moment-dmy.js
deleted file mode 100644 (file)
index 2b7ca16..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-// Use DMY date format for Moment.js, in accordance with MediaWiki's date formatting routines.
-// This affects English only (and languages without localisations, that fall back to English).
-// http://momentjs.com/docs/#/customization/long-date-formats/
-/* global moment */
-moment.updateLocale( 'en', {
-       longDateFormat: {
-               // Unchanged, but have to be repeated here:
-               LT: 'h:mm A',
-               LTS: 'h:mm:ss A',
-               // Customized:
-               L: 'DD/MM/YYYY',
-               LL: 'D MMMM YYYY',
-               LLL: 'D MMMM YYYY LT',
-               LLLL: 'dddd, D MMMM YYYY LT'
-       }
-} );
diff --git a/resources/src/moment-global.js b/resources/src/moment-global.js
deleted file mode 100644 (file)
index ba01a24..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-// Back-compat: Export module as global
-window.moment = module.exports;
diff --git a/resources/src/moment-locale-overrides.js b/resources/src/moment-locale-overrides.js
deleted file mode 100644 (file)
index bafb86a..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/* global mediaWiki, moment */
-
-( function ( mw ) {
-       // HACK: Overwrite moment's i18n with MediaWiki's for the current language so that
-       // wgTranslateNumerals is respected.
-       moment.updateLocale( moment.locale(), {
-               preparse: function ( s ) {
-                       var i,
-                               table = mw.language.getDigitTransformTable();
-                       if ( mw.config.get( 'wgTranslateNumerals' ) ) {
-                               for ( i = 0; i < 10; i++ ) {
-                                       if ( table[ i ] !== undefined ) {
-                                               s = s.replace( new RegExp( mw.RegExp.escape( table[ i ] ), 'g' ), i );
-                                       }
-                               }
-                       }
-                       // HACK: momentjs replaces commas in some languages, which is the only other use of preparse
-                       // aside from digit transformation. We can only override preparse, not extend it, so we
-                       // have to replicate the comma replacement functionality here.
-                       if ( [ 'ar', 'ar-sa', 'fa' ].indexOf( mw.config.get( 'wgUserLanguage' ) ) !== -1 ) {
-                               s = s.replace( /،/g, ',' );
-                       }
-                       return s;
-               },
-               postformat: function ( s ) {
-                       var i,
-                               table = mw.language.getDigitTransformTable();
-                       if ( mw.config.get( 'wgTranslateNumerals' ) ) {
-                               for ( i = 0; i < 10; i++ ) {
-                                       if ( table[ i ] !== undefined ) {
-                                               s = s.replace( new RegExp( i, 'g' ), table[ i ] );
-                                       }
-                               }
-                       }
-                       // HACK: momentjs replaces commas in some languages, which is the only other use of postformat
-                       // aside from digit transformation. We can only override postformat, not extend it, so we
-                       // have to replicate the comma replacement functionality here.
-                       if ( [ 'ar', 'ar-sa', 'fa' ].indexOf( mw.config.get( 'wgUserLanguage' ) ) !== -1 ) {
-                               s = s.replace( /,/g, '،' );
-                       }
-                       return s;
-               }
-       } );
-}( mediaWiki ) );
diff --git a/resources/src/moment/moment-dmy.js b/resources/src/moment/moment-dmy.js
new file mode 100644 (file)
index 0000000..2b7ca16
--- /dev/null
@@ -0,0 +1,16 @@
+// Use DMY date format for Moment.js, in accordance with MediaWiki's date formatting routines.
+// This affects English only (and languages without localisations, that fall back to English).
+// http://momentjs.com/docs/#/customization/long-date-formats/
+/* global moment */
+moment.updateLocale( 'en', {
+       longDateFormat: {
+               // Unchanged, but have to be repeated here:
+               LT: 'h:mm A',
+               LTS: 'h:mm:ss A',
+               // Customized:
+               L: 'DD/MM/YYYY',
+               LL: 'D MMMM YYYY',
+               LLL: 'D MMMM YYYY LT',
+               LLLL: 'dddd, D MMMM YYYY LT'
+       }
+} );
diff --git a/resources/src/moment/moment-global.js b/resources/src/moment/moment-global.js
new file mode 100644 (file)
index 0000000..ba01a24
--- /dev/null
@@ -0,0 +1,2 @@
+// Back-compat: Export module as global
+window.moment = module.exports;
diff --git a/resources/src/moment/moment-locale-overrides.js b/resources/src/moment/moment-locale-overrides.js
new file mode 100644 (file)
index 0000000..bafb86a
--- /dev/null
@@ -0,0 +1,44 @@
+/* global mediaWiki, moment */
+
+( function ( mw ) {
+       // HACK: Overwrite moment's i18n with MediaWiki's for the current language so that
+       // wgTranslateNumerals is respected.
+       moment.updateLocale( moment.locale(), {
+               preparse: function ( s ) {
+                       var i,
+                               table = mw.language.getDigitTransformTable();
+                       if ( mw.config.get( 'wgTranslateNumerals' ) ) {
+                               for ( i = 0; i < 10; i++ ) {
+                                       if ( table[ i ] !== undefined ) {
+                                               s = s.replace( new RegExp( mw.RegExp.escape( table[ i ] ), 'g' ), i );
+                                       }
+                               }
+                       }
+                       // HACK: momentjs replaces commas in some languages, which is the only other use of preparse
+                       // aside from digit transformation. We can only override preparse, not extend it, so we
+                       // have to replicate the comma replacement functionality here.
+                       if ( [ 'ar', 'ar-sa', 'fa' ].indexOf( mw.config.get( 'wgUserLanguage' ) ) !== -1 ) {
+                               s = s.replace( /،/g, ',' );
+                       }
+                       return s;
+               },
+               postformat: function ( s ) {
+                       var i,
+                               table = mw.language.getDigitTransformTable();
+                       if ( mw.config.get( 'wgTranslateNumerals' ) ) {
+                               for ( i = 0; i < 10; i++ ) {
+                                       if ( table[ i ] !== undefined ) {
+                                               s = s.replace( new RegExp( i, 'g' ), table[ i ] );
+                                       }
+                               }
+                       }
+                       // HACK: momentjs replaces commas in some languages, which is the only other use of postformat
+                       // aside from digit transformation. We can only override postformat, not extend it, so we
+                       // have to replicate the comma replacement functionality here.
+                       if ( [ 'ar', 'ar-sa', 'fa' ].indexOf( mw.config.get( 'wgUserLanguage' ) ) !== -1 ) {
+                               s = s.replace( /,/g, '،' );
+                       }
+                       return s;
+               }
+       } );
+}( mediaWiki ) );
index df4e67d..08ec9f6 100644 (file)
@@ -271,7 +271,6 @@ class ParserTestRunner {
                $setup['wgNoFollowLinks'] = true;
                $setup['wgNoFollowDomainExceptions'] = [ 'no-nofollow.org' ];
                $setup['wgExternalLinkTarget'] = false;
-               $setup['wgExperimentalHtmlIds'] = false;
                $setup['wgLocaltimezone'] = 'UTC';
                $setup['wgHtml5'] = true;
                $setup['wgDisableLangConversion'] = false;
index a04271f..8a40266 100644 (file)
@@ -133,6 +133,7 @@ class ApiParseTest extends ApiTestCase {
                                ->getMock();
                        $skin->expects( $this->once() )->method( 'getDefaultModules' )
                                ->willReturn( [
+                                       'styles' => [ 'core' => [ 'quux.styles' ] ],
                                        'core' => [ 'foo', 'bar' ],
                                        'content' => [ 'baz' ]
                                ] );
@@ -686,7 +687,7 @@ class ApiParseTest extends ApiTestCase {
                        'resp.parse.modulescripts'
                );
                $this->assertSame(
-                       [ 'foo.styles' ],
+                       [ 'foo.styles', 'quux.styles' ],
                        $res[0]['parse']['modulestyles'],
                        'resp.parse.modulestyles'
                );
index 6b41707..a1e41d9 100644 (file)
@@ -335,4 +335,42 @@ class DeferredUpdatesTest extends MediaWikiTestCase {
 
                $this->assertSame( 1, $ran, 'Update ran' );
        }
+
+       /**
+        * @covers DeferredUpdates::tryOpportunisticExecute
+        */
+       public function testTryOpportunisticExecute() {
+               $calls = [];
+               $callback1 = function () use ( &$calls ) {
+                       $calls[] = 1;
+               };
+               $callback2 = function () use ( &$calls ) {
+                       $calls[] = 2;
+               };
+
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $lbFactory->beginMasterChanges( __METHOD__ );
+
+               DeferredUpdates::addCallableUpdate( $callback1 );
+               $this->assertEquals( [], $calls );
+
+               DeferredUpdates::tryOpportunisticExecute( 'run' );
+               $this->assertEquals( [], $calls );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->onTransactionIdle( function () use ( &$calls, $callback2 ) {
+                       DeferredUpdates::addCallableUpdate( $callback2 );
+                       $this->assertEquals( [], $calls );
+                       $calls[] = 'oti';
+               } );
+               $this->assertEquals( 1, $dbw->trxLevel() );
+               $this->assertEquals( [], $calls );
+
+               $lbFactory->commitMasterChanges( __METHOD__ );
+
+               $this->assertEquals( [ 'oti' ], $calls );
+
+               DeferredUpdates::tryOpportunisticExecute( 'run' );
+               $this->assertEquals( [ 'oti', 1, 2 ], $calls );
+       }
 }
index 6590338..b5965c4 100644 (file)
@@ -457,7 +457,6 @@ class SanitizerTest extends MediaWikiTestCase {
                $legacyEncoded = 'foo_.D1.82.D0.B5.D1.81.D1.82_.23.25.21.27.28.29.5B.5D:.3C.3E' .
                        '.26.26amp.3B.26amp.3Bamp.3B';
                $html5Encoded = 'foo_тест_#%!\'()[]:<>&&amp;&amp;amp;';
-               $html5Experimental = 'foo_тест_!_()[]:<>_amp;_amp;amp;';
 
                // Settings: last element is $wgExternalInterwikiFragmentMode, the rest is $wgFragmentMode
                $legacy = [ 'legacy', 'legacy' ];
@@ -465,8 +464,6 @@ class SanitizerTest extends MediaWikiTestCase {
                $newLegacy = [ 'html5', 'legacy', 'legacy' ];
                $new = [ 'html5', 'legacy' ];
                $allNew = [ 'html5', 'html5' ];
-               $experimentalLegacy = [ 'html5-legacy', 'legacy', 'legacy' ];
-               $newExperimental = [ 'html5', 'html5-legacy', 'legacy' ];
 
                return [
                        // Pure legacy: how MW worked before 2017
@@ -498,18 +495,6 @@ class SanitizerTest extends MediaWikiTestCase {
                        [ 'Attribute', $allNew, $text, false, Sanitizer::ID_FALLBACK ],
                        [ 'Link', $allNew, $text, $html5Encoded ],
                        [ 'ExternalInterwiki', $allNew, $text, $html5Encoded ],
-
-                       // Someone flipped $wgExperimentalHtmlIds on
-                       [ 'Attribute', $experimentalLegacy, $text, $html5Experimental, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $experimentalLegacy, $text, $legacyEncoded, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $experimentalLegacy, $text, $html5Experimental ],
-                       [ 'ExternalInterwiki', $experimentalLegacy, $text, $legacyEncoded ],
-
-                       // Migration from $wgExperimentalHtmlIds to modern HTML5
-                       [ 'Attribute', $newExperimental, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $newExperimental, $text, $html5Experimental, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $newExperimental, $text, $html5Encoded ],
-                       [ 'ExternalInterwiki', $newExperimental, $text, $legacyEncoded ],
                ];
        }
 
index a1b1422..61ab8a5 100644 (file)
@@ -71,8 +71,7 @@ CSS
 
        /**
         * @dataProvider provideGetStyles
-        * @covers ResourceLoaderSkinModule::normalizeStyles
-        * @covers ResourceLoaderSkinModule::getStyles
+        * @covers ResourceLoaderSkinModule
         */
        public function testGetStyles( $parent, $logo, $expected ) {
                $module = $this->getMockBuilder( ResourceLoaderSkinModule::class )
index 4e9f539..e811d87 100644 (file)
@@ -99,6 +99,22 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
                $resourceLoader->register( 'test', new stdClass() );
        }
 
+       /**
+        * @covers ResourceLoader::register
+        */
+       public function testRegisterDuplicate() {
+               $logger = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock();
+               $logger->expects( $this->once() )
+                       ->method( 'warning' );
+               $resourceLoader = new EmptyResourceLoader( null, $logger );
+
+               $module1 = new ResourceLoaderTestModule();
+               $module2 = new ResourceLoaderTestModule();
+               $resourceLoader->register( 'test', $module1 );
+               $resourceLoader->register( 'test', $module2 );
+               $this->assertSame( $module2, $resourceLoader->getModule( 'test' ) );
+       }
+
        /**
         * @covers ResourceLoader::getModuleNames
         */
index 06b0667..b746fc3 100644 (file)
@@ -59,7 +59,6 @@ class SkinTemplateTest extends MediaWikiTestCase {
                $defaultStyles = [
                        'mediawiki.legacy.shared',
                        'mediawiki.legacy.commonPrint',
-                       'mediawiki.sectionAnchor',
                ];
                $buttonStyle = 'mediawiki.ui.button';
                $feedStyle = 'mediawiki.feedlink';
index 7c99614..5a554a0 100644 (file)
@@ -57,19 +57,59 @@ class LanguageCrhTest extends LanguageClassesTestCase {
                        ],
                        [ // recent problem words, part 1
                                [
-                                       'crh'      => 'künü куню sürgünligi сюргюнлиги özü озю etti этти',
-                                       'crh-cyrl' => 'куню куню сюргюнлиги сюргюнлиги озю озю этти этти',
-                                       'crh-latn' => 'künü künü sürgünligi sürgünligi özü özü etti etti',
+                                       'crh'      => 'künü куню sürgünligi сюргюнлиги özü озю etti этти esas эсас dört дёрт',
+                                       'crh-cyrl' => 'куню куню сюргюнлиги сюргюнлиги озю озю этти этти эсас эсас дёрт дёрт',
+                                       'crh-latn' => 'künü künü sürgünligi sürgünligi özü özü etti etti esas esas dört dört',
                                ],
-                               'künü куню sürgünligi сюргюнлиги özü озю etti этти'
+                               'künü куню sürgünligi сюргюнлиги özü озю etti этти esas эсас dört дёрт'
                        ],
                        [ // recent problem words, part 2
                                [
-                                       'crh'      => 'esas эсас dört дёрт keldi кельди',
-                                       'crh-cyrl' => 'эсас эсас дёрт дёрт кельди кельди',
-                                       'crh-latn' => 'esas esas dört dört keldi keldi',
+                                       'crh'      => 'keldi кельди km² км² yüz юзь AQŞ АКъШ ŞSCBnen ШСДжБнен iyül июль',
+                                       'crh-cyrl' => 'кельди кельди км² км² юзь юзь АКъШ АКъШ ШСДжБнен ШСДжБнен июль июль',
+                                       'crh-latn' => 'keldi keldi km² km² yüz yüz AQŞ AQŞ ŞSCBnen ŞSCBnen iyül iyül',
                                ],
-                               'esas эсас dört дёрт keldi кельди'
+                               'keldi кельди km² км² yüz юзь AQŞ АКъШ ŞSCBnen ШСДжБнен iyül июль'
+                       ],
+                       [ // recent problem words, part 3
+                               [
+                                       'crh'      => 'işğal ишгъаль işğalcilerine ишгъальджилерине rayon район üst усть',
+                                       'crh-cyrl' => 'ишгъаль ишгъаль ишгъальджилерине ишгъальджилерине район район усть усть',
+                                       'crh-latn' => 'işğal işğal işğalcilerine işğalcilerine rayon rayon üst üst',
+                               ],
+                               'işğal ишгъаль işğalcilerine ишгъальджилерине rayon район üst усть'
+                       ],
+                       [ // recent problem words, part 4
+                               [
+                                       'crh'      => 'rayonınıñ районынынъ Noğay Ногъай Yürtü Юрьтю vatandan ватандан',
+                                       'crh-cyrl' => 'районынынъ районынынъ Ногъай Ногъай Юрьтю Юрьтю ватандан ватандан',
+                                       'crh-latn' => 'rayonınıñ rayonınıñ Noğay Noğay Yürtü Yürtü vatandan vatandan',
+                               ],
+                               'rayonınıñ районынынъ Noğay Ногъай Yürtü Юрьтю vatandan ватандан'
+                       ],
+                       [ // recent problem words, part 5
+                               [
+                                       'crh'      => 'ком-кок köm-kök rol роль AQQI АКЪКЪЫ DAĞĞA ДАГЪГЪА 13-ünci 13-юнджи',
+                                       'crh-cyrl' => 'ком-кок ком-кок роль роль АКЪКЪЫ АКЪКЪЫ ДАГЪГЪА ДАГЪГЪА 13-юнджи 13-юнджи',
+                                       'crh-latn' => 'köm-kök köm-kök rol rol AQQI AQQI DAĞĞA DAĞĞA 13-ünci 13-ünci',
+                               ],
+                               'ком-кок köm-kök rol роль AQQI АКЪКЪЫ DAĞĞA ДАГЪГЪА 13-ünci 13-юнджи'
+                       ],
+                       [ // recent problem words, part 6
+                               [
+                                       'crh'      => 'ДЖУРЬМЕК CÜRMEK кетсин ketsin джумлеси cümlesi ильи ilyi Ильи İlyi',
+                                       'crh-cyrl' => 'ДЖУРЬМЕК ДЖУРЬМЕК кетсин кетсин джумлеси джумлеси ильи ильи Ильи Ильи',
+                                       'crh-latn' => 'CÜRMEK CÜRMEK ketsin ketsin cümlesi cümlesi ilyi ilyi İlyi İlyi',
+                               ],
+                               'ДЖУРЬМЕК CÜRMEK кетсин ketsin джумлеси cümlesi ильи ilyi Ильи İlyi'
+                       ],
+                       [ // regex pattern words
+                               [
+                                       'crh'      => 'köyünden коюнден ange аньге',
+                                       'crh-cyrl' => 'коюнден коюнден аньге аньге',
+                                       'crh-latn' => 'köyünden köyünden ange ange',
+                               ],
+                               'köyünden коюнден ange аньге'
                        ],
                        [ // multi part words
                                [
@@ -79,13 +119,61 @@ class LanguageCrhTest extends LanguageClassesTestCase {
                                ],
                                'эки юз eki yüz'
                        ],
-                       [ // ALL CAPS, made up acronyms (not 100% sure these are correct)
+                       [ // affix patterns
                                [
-                                       'crh'      => 'ÑAB QIC ĞUK COT НЪАБ КЪЫДж ГЪУК ДЖОТ CA ДЖА',
-                                       'crh-cyrl' => 'НЪАБ КЪЫДж ГЪУК ДЖОТ НЪАБ КЪЫДж ГЪУК ДЖОТ ДЖА ДЖА',
+                                       'crh'      => 'köyniñ койнинъ Avcıköyde Авджыкойде ekvatorial экваториаль Canköy Джанкой',
+                                       'crh-cyrl' => 'койнинъ койнинъ Авджыкойде Авджыкойде экваториаль экваториаль Джанкой Джанкой',
+                                       'crh-latn' => 'köyniñ köyniñ Avcıköyde Avcıköyde ekvatorial ekvatorial Canköy Canköy',
+                               ],
+                               'köyniñ койнинъ Avcıköyde Авджыкойде ekvatorial экваториаль Canköy Джанкой'
+                       ],
+                       [ // Roman numerals and quotes, esp. single-letter Roman numerals at the end of a string
+                               [
+                                       'crh'      => 'VI,VII IX “dört” «дёрт» XI XII I V X L C D M',
+                                       'crh-cyrl' => 'VI,VII IX «дёрт» «дёрт» XI XII I V X L C D M',
+                                       'crh-latn' => 'VI,VII IX “dört” "dört" XI XII I V X L C D M',
+                               ],
+                               'VI,VII IX “dört” «дёрт» XI XII I V X L C D M'
+                       ],
+                       [ // Roman numerals vs Initials, part 1 - Roman numeral initials without spaces
+                               [
+                                       'crh'      => 'A.B.C.D.M. Qadırova XII, А.Б.Дж.Д.М. Къадырова XII',
+                                       'crh-cyrl' => 'А.Б.Дж.Д.М. Къадырова XII, А.Б.Дж.Д.М. Къадырова XII',
+                                       'crh-latn' => 'A.B.C.D.M. Qadırova XII, A.B.C.D.M. Qadırova XII',
+                               ],
+                               'A.B.C.D.M. Qadırova XII, А.Б.Дж.Д.М. Къадырова XII'
+                       ],
+                       [ // Roman numerals vs Initials, part 2 - Roman numeral initials with spaces
+                               [
+                                       'crh'      => 'G. H. I. V. X. L. Memetov III, Г. Х. Ы. В. X. Л. Меметов III',
+                                       'crh-cyrl' => 'Г. Х. Ы. В. X. Л. Меметов III, Г. Х. Ы. В. X. Л. Меметов III',
+                                       'crh-latn' => 'G. H. I. V. X. L. Memetov III, G. H. I. V. X. L. Memetov III',
+                               ],
+                               'G. H. I. V. X. L. Memetov III, Г. Х. Ы. В. X. Л. Меметов III'
+                       ],
+                       [ // ALL CAPS, made up acronyms
+                               [
+                                       'crh'      => 'ÑAB QIC ĞUK COT НЪАБ КЪЫДЖ ГЪУК ДЖОТ CA ДЖА',
+                                       'crh-cyrl' => 'НЪАБ КЪЫДЖ ГЪУК ДЖОТ НЪАБ КЪЫДЖ ГЪУК ДЖОТ ДЖА ДЖА',
                                        'crh-latn' => 'ÑAB QIC ĞUK COT ÑAB QIC ĞUK COT CA CA',
                                ],
-                               'ÑAB QIC ĞUK COT НЪАБ КЪЫДж ГЪУК ДЖОТ CA ДЖА'
+                               'ÑAB QIC ĞUK COT НЪАБ КЪЫДЖ ГЪУК ДЖОТ CA ДЖА'
+                       ],
+                       [ // Many-to-one mappings: many Cyrillic to one Latin
+                               [
+                                       'crh'      => 'шофер шофёр şoför корбекул корьбекул корьбекуль körbekül',
+                                       'crh-cyrl' => 'шофер шофёр шофёр корбекул корьбекул корьбекуль корьбекуль',
+                                       'crh-latn' => 'şoför şoför şoför körbekül körbekül körbekül körbekül',
+                               ],
+                               'шофер шофёр şoför корбекул корьбекул корьбекуль körbekül'
+                       ],
+                       [ // Many-to-one mappings: many Latin to one Cyrillic
+                               [
+                                       'crh'      => 'fevqülade fevqulade февкъульаде beyude beyüde бейуде',
+                                       'crh-cyrl' => 'февкъульаде февкъульаде февкъульаде бейуде бейуде бейуде',
+                                       'crh-latn' => 'fevqülade fevqulade fevqulade beyude beyüde beyüde',
+                               ],
+                               'fevqülade fevqulade февкъульаде beyude beyüde бейуде'
                        ],
                ];
        }
index 0653dfd..71362fd 100644 (file)
        } );
 
        QUnit.test( 'Integration', function ( assert ) {
-               var expected, logSpy, msg;
+               var expected, msg;
 
                expected = '<b><a title="Bold" href="/wiki/Bold">Bold</a>!</b>';
                mw.messages.set( 'integration-test', '<b>[[Bold]]!</b>' );
 
-               this.suppressWarnings();
-               logSpy = this.sandbox.spy( mw.log, 'warn' );
-               assert.equal(
-                       window.gM( 'integration-test' ),
-                       expected,
-                       'Global function gM() works correctly'
-               );
-               assert.equal( logSpy.callCount, 1, 'mw.log.warn called' );
-               this.restoreWarnings();
-
                assert.equal(
                        mw.message( 'integration-test' ).parse(),
                        expected,
index b8464e9..5508088 100644 (file)
                var text = 'foo тест_#%!\'()[]:<>',
                        legacyEncoded = 'foo_.D1.82.D0.B5.D1.81.D1.82_.23.25.21.27.28.29.5B.5D:.3C.3E',
                        html5Encoded = 'foo_тест_#%!\'()[]:<>',
-                       html5Experimental = 'foo_тест_!_()[]:<>',
                        // Settings: this is $wgFragmentMode
                        legacy = [ 'legacy' ],
                        legacyNew = [ 'legacy', 'html5' ],
                        newLegacy = [ 'html5', 'legacy' ],
-                       allNew = [ 'html5' ],
-                       experimentalLegacy = [ 'html5-legacy', 'legacy' ],
-                       newExperimental = [ 'html5', 'html5-legacy' ];
+                       allNew = [ 'html5' ];
 
                // Test cases are kept in sync with SanitizerTest.php
                [
                        // New world: HTML5 links, legacy fallbacks
                        [ newLegacy, text, html5Encoded ],
                        // Distant future: no legacy fallbacks
-                       [ allNew, text, html5Encoded ],
-                       // Someone flipped $wgExperimentalHtmlIds on
-                       [ experimentalLegacy, text, html5Experimental ],
-                       // Migration from $wgExperimentalHtmlIds to modern HTML5
-                       [ newExperimental, text, html5Encoded ]
+                       [ allNew, text, html5Encoded ]
                ].forEach( function ( testCase ) {
                        mw.config.set( 'wgFragmentMode', testCase[ 0 ] );
 
                var text = 'foo тест_#%!\'()[]:<>',
                        legacyEncoded = 'foo_.D1.82.D0.B5.D1.81.D1.82_.23.25.21.27.28.29.5B.5D:.3C.3E',
                        html5Encoded = 'foo_тест_#%!\'()[]:<>',
-                       html5Experimental = 'foo_тест_!_()[]:<>',
                        // Settings: this is wgFragmentMode
                        legacy = [ 'legacy' ],
                        legacyNew = [ 'legacy', 'html5' ],
                        newLegacy = [ 'html5', 'legacy' ],
-                       allNew = [ 'html5' ],
-                       experimentalLegacy = [ 'html5-legacy', 'legacy' ],
-                       newExperimental = [ 'html5', 'html5-legacy' ];
+                       allNew = [ 'html5' ];
 
                [
                        // Pure legacy: how MW worked before 2017
                        // New world: HTML5 links, legacy fallbacks
                        [ newLegacy, text, html5Encoded ],
                        // Distant future: no legacy fallbacks
-                       [ allNew, text, html5Encoded ],
-                       // Someone flipped wgExperimentalHtmlIds on
-                       [ experimentalLegacy, text, html5Experimental ],
-                       // Migration from wgExperimentalHtmlIds to modern HTML5
-                       [ newExperimental, text, html5Encoded ]
+                       [ allNew, text, html5Encoded ]
                ].forEach( function ( testCase ) {
                        mw.config.set( 'wgFragmentMode', testCase[ 0 ] );
 
index 85fc310..e39226c 100644 (file)
@@ -9,6 +9,6 @@
                "browser": false
        },
        "rules":{
-               "no-console":0
+               "no-console": 0
        }
 }
index b15d407..274eb14 100644 (file)
@@ -5,9 +5,8 @@
 - [Chrome](https://www.google.com/chrome/)
 - [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/)
 - [Node.js](https://nodejs.org/en/)
-- [MediaWiki-Vagrant](https://www.mediawiki.org/wiki/MediaWiki-Vagrant)
 
-Set up MediaWiki-Vagrant:
+If using MediaWiki-Vagrant:
 
     cd mediawiki/vagrant
     vagrant up
@@ -24,7 +23,7 @@ Set up MediaWiki-Vagrant:
 By default, Chrome will run in headless mode. If you want to see Chrome, set DISPLAY
 environment variable to any value:
 
-    DISPLAY=:1 npm run selenium
+    DISPLAY=1 npm run selenium
 
 To run only one file (for example page.js), you first need to spawn the chromedriver:
 
index 6b71019..4a5c254 100755 (executable)
@@ -1,5 +1,8 @@
 #!/usr/bin/env bash
 set -euo pipefail
+# Check the command before running in background so
+# that it can actually fail and have a descriptive error
+hash chromedriver
 chromedriver --url-base=/wd/hub --port=4444 &
 # Make sure it is killed to prevent file descriptors leak
 function kill_chromedriver() {